Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found

Target

Select target project
  • aura/engine
  • hermannschwaerzler/engine
  • sumpfralle/aura-engine
3 results
Show changes
Showing
with 3761 additions and 0 deletions
if not icecast_vorbis_metadata then
source := add(normalize=false, [amplify(0.00001, noise()), !source])
end
if bitrate == 24 or bitrate == 32 or bitrate == 48 then
if stereo then
ignore(output_icecast_stereo(%flac(samplerate=44100, channels = 2, compression = 7, bits_per_sample=8), !source))
else
ignore(output_icecast_mono(%flac(samplerate=44100, channels = 1, compression = 7, bits_per_sample=8), mean(!source)))
end
elsif bitrate == 64 then
if stereo then
ignore(output_icecast_stereo(%flac(samplerate=44100, channels = 2, compression = 7, bits_per_sample=8), !source))
else
ignore(output_icecast_mono(%flac(samplerate=44100, channels = 1, compression = 7, bits_per_sample=8), mean(!source)))
end
elsif bitrate == 96 then
if stereo then
ignore(output_icecast_stereo(%flac(samplerate=44100, channels = 2, compression = 7, bits_per_sample=8), !source))
else
ignore(output_icecast_mono(%flac(samplerate=44100, channels = 1, compression = 7, bits_per_sample=8), mean(!source)))
end
elsif bitrate == 128 then
if stereo then
ignore(output_icecast_stereo(%flac(samplerate=44100, channels = 2, compression = 6, bits_per_sample=16), !source))
else
ignore(output_icecast_mono(%flac(samplerate=44100, channels = 1, compression = 6, bits_per_sample=16), mean(!source)))
end
elsif bitrate == 160 then
if stereo then
ignore(output_icecast_stereo(%flac(samplerate=44100, channels = 2, compression = 5, bits_per_sample=16), !source))
else
ignore(output_icecast_mono(%flac(samplerate=44100, channels = 1, compression = 5, bits_per_sample=16), mean(!source)))
end
elsif bitrate == 192 then
if stereo then
ignore(output_icecast_stereo(%flac(samplerate=44100, channels = 2, compression = 4, bits_per_sample=16), !source))
else
ignore(output_icecast_mono(%flac(samplerate=44100, channels = 1, compression = 4, bits_per_sample=16), mean(!source)))
end
elsif bitrate == 224 then
if stereo then
ignore(output_icecast_stereo(%flac(samplerate=44100, channels = 2, compression = 3, bits_per_sample=32), !source))
else
ignore(output_icecast_mono(%flac(samplerate=44100, channels = 1, compression = 3, bits_per_sample=32), mean(!source)))
end
elsif bitrate == 256 then
if stereo then
ignore(output_icecast_stereo(%flac(samplerate=44100, channels = 2, compression = 2, bits_per_sample=32), !source))
else
ignore(output_icecast_mono(%flac(samplerate=44100, channels = 1, compression = 2, bits_per_sample=32), mean(!source)))
end
elsif bitrate == 320 then
if stereo then
ignore(output_icecast_stereo(%flac(samplerate=44100, channels = 2, compression = 1, bits_per_sample=32), !source))
else
ignore(output_icecast_mono(%flac(samplerate=44100, channels = 1, compression = 1, bits_per_sample=32), mean(!source)))
end
end
if bitrate == 24 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 24, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 24, stereo = false), mean(!source)))
end
elsif bitrate == 32 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 32, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 32, stereo = false), mean(!source)))
end
elsif bitrate == 48 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 48, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 48, stereo = false), mean(!source)))
end
elsif bitrate == 64 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 64, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 64, stereo = false), mean(!source)))
end
elsif bitrate == 96 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 96, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 96, stereo = false), mean(!source)))
end
elsif bitrate == 128 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 128, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 128, stereo = false), mean(!source)))
end
elsif bitrate == 160 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 160, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 160, stereo = false), mean(!source)))
end
elsif bitrate == 192 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 192, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 192, stereo = false), mean(!source)))
end
elsif bitrate == 224 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 224, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 224, stereo = false), mean(!source)))
end
elsif bitrate == 256 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 256, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 256, stereo = false), mean(!source)))
end
elsif bitrate == 320 then
if stereo then
ignore(output_icecast_stereo(%mp3(bitrate = 320, stereo = true), !source))
else
ignore(output_icecast_mono(%mp3(bitrate = 320, stereo = false), mean(!source)))
end
end
if not icecast_vorbis_metadata then
source := add(normalize=false, [amplify(0.00001, noise()), !source])
end
if bitrate == 24 or bitrate == 32 or bitrate == 48 then
if stereo then
ignore(output_icecast_stereo(%vorbis(quality=-0.1, channels = 2), !source))
else
ignore(output_icecast_mono(%vorbis(quality=-0.1, channels = 1), mean(!source)))
end
elsif bitrate == 64 then
if stereo then
ignore(output_icecast_stereo(%vorbis(quality=0, channels = 2), !source))
else
ignore(output_icecast_mono(%vorbis(quality=0, channels = 1), mean(!source)))
end
elsif bitrate == 96 then
if stereo then
ignore(output_icecast_stereo(%vorbis(quality=0.2, channels = 2), !source))
else
ignore(output_icecast_mono(%vorbis(quality=0.2, channels = 1), mean(!source)))
end
elsif bitrate == 128 then
if stereo then
ignore(output_icecast_stereo(%vorbis(quality=0.4, channels = 2), !source))
else
ignore(output_icecast_mono(%vorbis(quality=0.4, channels = 1), mean(!source)))
end
elsif bitrate == 160 then
if stereo then
ignore(output_icecast_stereo(%vorbis(quality=0.5, channels = 2), !source))
else
ignore(output_icecast_mono(%vorbis(quality=0.5, channels = 1), mean(!source)))
end
elsif bitrate == 192 then
if stereo then
ignore(output_icecast_stereo(%vorbis(quality=0.6, channels = 2), !source))
else
ignore(output_icecast_mono(%vorbis(quality=0.6, channels = 1), mean(!source)))
end
elsif bitrate == 224 then
if stereo then
ignore(output_icecast_stereo(%vorbis(quality=0.7, channels = 2), !source))
else
ignore(output_icecast_mono(%vorbis(quality=0.7, channels = 1), mean(!source)))
end
elsif bitrate == 256 then
if stereo then
ignore(output_icecast_stereo(%vorbis(quality=0.8, channels = 2), !source))
else
ignore(output_icecast_mono(%vorbis(quality=0.8, channels = 1), mean(!source)))
end
elsif bitrate == 320 then
if stereo then
ignore(output_icecast_stereo(%vorbis(quality=0.9, channels = 2), !source))
else
ignore(output_icecast_mono(%vorbis(quality=0.9, channels = 1), mean(!source)))
end
end
if bitrate == 24 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 24, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 24, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 32 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 32, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 32, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 48 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 48, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 48, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 64 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 64, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 64, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 96 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 96, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 96, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 128 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 128, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 128, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 160 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 160, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 160, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 192 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 192, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 192, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 224 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 224, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 224, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 256 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 256, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 256, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
elsif bitrate == 320 then
if stereo then
ignore(output_icecast_stereo(%opus(bitrate = 320, channels = 2, signal="music", application="audio", complexity=10, vbr="constrained"), !source))
else
ignore(output_icecast_mono(%opus(bitrate = 320, channels = 1, signal="music", application="audio", complexity=10, vbr="constrained"), mean(!source)))
end
end
#
# 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/>.
#
#################
# Read INI File #
#################
def read_ini(file)
ret = get_process_lines("cat "^file )
ret = list.map(string.split(separator="="), ret)
# l' => the filling list
def f(l',l) =
if list.length(l) >= 2 then
line = string.extract(pattern='"(.*)"', list.nth(l,1))
#print(line)
#print((list.hd(l),line['1']))
list.append([(list.hd(l),line['1'])],l')
else
if list.length(l) >= 1 then
list.append([(list.hd(l),"")],l')
else
l'
end
end
end
# append install dir
pwd = get_process_lines("pwd")
install_dir = dirname(dirname(list.hd(pwd)))
list.fold(f, [("install_dir", install_dir)], ret)
end
\ No newline at end of file
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
# shutdown server function
#server.register(namespace='server',
# description="shutdown server",
# usage="stop",
# "stop",
# fun(x,y) -> shutdown )
#
# to reduce complexity of lqs => query 'mixer.inputs' over socket and parse it in python
#server.register(namespace="auraengine",
# "enabled_lineins",
# fun (s) -> begin
# log("auraengine.enabled_lineins")
# "0:#{!linein_0_enabled}, 1:#{!linein_1_enabled}, 2:#{!linein_2_enabled}, 3:#{!linein_3_enabled}, 4:#{!linein_4_enabled}"
# end
#)
#server.register(namespace="auraengine",
# "enabled_lineouts",
# fun(s) -> begin
# log("auraengine.enabled_lineouts")
# "0:#{!lineout_0_enabled}, 1:#{!lineout_1_enabled}, 2:#{!lineout_2_enabled}, 3:#{!lineout_3_enabled}, 4:#{!lineout_4_enabled}"
# end
#)
# are outgoing streams connected?
server.register(namespace="auraengine",
description="returns if outgoing streams are connected",
usage="out_streams_connected",
"out_streams_connected",
fun (s) -> begin
log("streams.connection_status")
"0:#{!s0_connected}, 1:#{!s1_connected}, 2:#{!s2_connected}, 3:#{!s3_connected}, 4:#{!s4_connected}"
end
)
# return a state of the inputs/outputs of the soundserver as JSON
server.register(namespace = "auraengine",
description="returns enabled lineouts/lineins, connected outgoing streams, and recorder. Also returns fallbacksettings.",
usage="state",
"state",
fun(s) -> begin
log("auraengine.state")
ret = '{'
ret = ret^'"streams": {'
ret = ret^'"stream_0": {"enabled": #{s0_enable}, "connected": #{!s0_connected}},'
ret = ret^'"stream_1": {"enabled": #{s1_enable}, "connected": #{!s1_connected}},'
ret = ret^'"stream_2": {"enabled": #{s2_enable}, "connected": #{!s2_connected}},'
ret = ret^'"stream_3": {"enabled": #{s3_enable}, "connected": #{!s3_connected}},'
ret = ret^'"stream_4": {"enabled": #{s4_enable}, "connected": #{!s4_connected}}'
ret = ret^'},'
ret = ret^'"recorder": {'
ret = ret^'"recorder_0": {"enabled": #{r0_enable}, "recording": #{!r0_is_recording}},'
ret = ret^'"recorder_1": {"enabled": #{r1_enable}, "recording": #{!r1_is_recording}},'
ret = ret^'"recorder_2": {"enabled": #{r2_enable}, "recording": #{!r2_is_recording}},'
ret = ret^'"recorder_3": {"enabled": #{r3_enable}, "recording": #{!r3_is_recording}},'
ret = ret^'"recorder_4": {"enabled": #{r4_enable}, "recording": #{!r4_is_recording}}'
ret = ret^'},'
ret = ret^'"linein": {'
ret = ret^'"linein_0": {"enabled": #{a0_in != ""}},'
ret = ret^'"linein_1": {"enabled": #{a1_in != ""}},'
ret = ret^'"linein_2": {"enabled": #{a2_in != ""}},'
ret = ret^'"linein_3": {"enabled": #{a3_in != ""}},'
ret = ret^'"linein_4": {"enabled": #{a4_in != ""}}'
ret = ret^'},'
ret = ret^'"lineout": {'
ret = ret^'"lineout_0": {"enabled": #{a0_out != ""}},'
ret = ret^'"lineout_1": {"enabled": #{a1_out != ""}},'
ret = ret^'"lineout_2": {"enabled": #{a2_out != ""}},'
ret = ret^'"lineout_3": {"enabled": #{a3_out != ""}},'
ret = ret^'"lineout_4": {"enabled": #{a4_out != ""}}'
ret = ret^'}'
ret = ret^'}'
ret
# outgoing streams enabled?
#ret = "stream_0_enabled:#{!s0_enable}, stream_1_enabled:#{!s1_enable}, stream_2_enabled:#{!s2_enable}, stream_3_enabled:#{!s3_enable}, stream_4_enabled:#{!s4_enable}, "
# outgoing recorder enabled
#ret = ret^"recorder_0_enabled:#{r0_enable}, recorder_1_enabled:#{r1_enable}, recorder_2_enabled:#{r2_enable}, recorder_3_enabled:#{r3_enable}, recorder_4_enabled:#{r4_enable}, "
#ret = ret^"linein_0_enabled:#{a0_in != ''}, linein_1_enabled:#{a1_in != ''}, linein_2_enabled:#{a2_in != ''}, linein_3_enabled:#{a3_in != ''}, linein_4_enabled:#{a4_in != ''}, "
#ret = ret^"lineout_0_enabled:#{a0_out != ''}, lineout_1_enabled:#{a1_out != ''}, lineout_2_enabled:#{a2_out != ''}, lineout_3_enabled:#{a3_out != ''}, lineout_4_enabled:#{a4_out != ''}, "
#ret = ret^"fallback_max_blank:#{fallback_max_blank}, fallback_min_noise:#{fallback_min_noise}, fallback_threshold:#{fallback_threshold}"
end
)
# enable the seek function for the input from the filesystem
server.register(namespace = source.id(input_fs),
description="seek to relative position in #{source.id(input_fs)}",
usage = "seek <duration in seconds>",
"seek",
fun(t) -> begin
log("Seeking #{t} sec")
t = float_of_string(default=0.,t)
ret = source.seek(input_fs, t)
"Seeked #{ret} seconds."
end
)
def fadeTo(source_number) =
if source_number == "" then
print(source_number)
"Usage: mixer.fadeto <source nb> #{source_number}"
else
r = server.execute("mixer.select #{source_number} true")
print(r)
"Donee!"
end
end
# enable fadeTo for the mixer
server.register(namespace = "mixer",
description = "is fading from one mixer input to another",
usage = "fadeto <source number>",
"fadeto",
fadeTo
)
ignore(fade_in_time)
ignore(fade_out_time)
\ 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/>.
#
# READ INI FILE
%include "readini.liq"
ini = read_ini("../../configuration/engine.ini")
# TELNET SETTINGS
set("server.telnet", true)
set("server.telnet.bind_addr", "0.0.0.0")
set("server.telnet.port", 1234)
# LOG FILE SETTINGS
set("log.file.path", "./<script>.log")
# SOCKET SETTINGS
set("server.socket", true)
set("server.socket.path", "./<script>.sock")
# SOUND CARD SETTINGS
a0_in = list.assoc("input_device_0", ini)
a1_in = list.assoc("input_device_1", ini)
a2_in = list.assoc("input_device_2", ini)
a3_in = list.assoc("input_device_3", ini)
a4_in = list.assoc("input_device_4", ini)
a0_out = list.assoc("output_device_0", ini)
a1_out = list.assoc("output_device_1", ini)
a2_out = list.assoc("output_device_2", ini)
a3_out = list.assoc("output_device_3", ini)
a4_out = list.assoc("output_device_4", ini)
# FALLBACK SETTINGS
fallback_max_blank = float_of_string(list.assoc("fallback_max_blank", ini))
fallback_min_noise = float_of_string(list.assoc("fallback_min_noise", ini))
fallback_threshold = float_of_string(list.assoc("fallback_threshold", ini))
# FADING SETTINGS
fade_in_time = list.assoc("fade_in_time", ini) #int_of_string(list.assoc("fade_in_time", ini))
fade_out_time = list.assoc("fade_out_time", ini) #int_of_string(list.assoc("fade_out_time", ini))
# RECORDER SETTINGS
#rec_0_filetype = list.assoc("rec_0_filetype", ini)
#rec_1_filetype = list.assoc("rec_1_filetype", ini)
#rec_2_filetype = list.assoc("rec_2_filetype", ini)
#rec_3_filetype = list.assoc("rec_3_filetype", ini)
#rec_4_filetype = list.assoc("rec_4_filetype", ini)
# ALSA / pulse settings
soundsystem = list.assoc("soundsystem", ini)
use_alsa = soundsystem == "alsa"
use_jack = soundsystem == "jack"
if use_alsa then
frame_duration = float_of_string(list.assoc("frame_duration", ini))
frame_size = int_of_string(list.assoc("frame_size", ini))
alsa_buffer = int_of_string(list.assoc("alsa_buffer", ini))
alsa_buffer_length = int_of_string(list.assoc("alsa_buffer_length", ini))
alsa_periods = int_of_string(list.assoc("alsa_periods", ini))
if frame_duration > 0.0 then
print("setting frame.duration to #{frame_duration}s")
set("frame.duration", frame_duration)
end
if frame_size > 0 then
print("setting frame.size to #{frame_size}")
set("frame.audio.size", frame_size)
end
if alsa_buffer > 0 then
print("setting alsa.buffer to #{alsa_buffer}")
set("alsa.alsa_buffer", alsa_buffer)
end
if alsa_buffer > 0 then
print("setting alsa.buffer_length to #{alsa_buffer_length}")
set("alsa.buffer_length", alsa_buffer_length)
end
if alsa_periods > 0 then
print("setting alsa.periods to #{alsa_periods}")
set("alsa.periods", alsa_periods) # assertion error when setting periods other than 0 => alsa default
end
end
# %include "readini.liq"
# ini = read_ini("/etc/aura/engine.ini")
# %include "settings.liq"
# TELNET SETTINGS
# set("server.telnet", true)
# set("server.telnet.bind_addr", "0.0.0.0")
# set("server.telnet.port", 2345)
inst = if argv(1) != "" then string_of(argv(1)) else 'record' end
instance = ref inst
audiobase = if !instance == 'record' then list.assoc("audiobase", ini) else list.assoc("altaudiobase", ini) end
rec_filetype = list.assoc("rec_filetype", ini)
filenamepattern = ref audiobase^"/%Y-%m-%d/%Y-%m-%d-%H-%M.flac"
# Der aktuelle Dateiname für die Aufnahme
recordingfile = ref ""
# wir definieren eine Referenz für die Stop-Funktion, die aber bisher noch nichts tun kann
stop_f = ref (fun () -> ())
# bewahre uns davor, dass zweimal gleichzeitig die gleiche Date aufgenommen wird
is_record_active = ref false
# Stop dump - wir definieren die Funktion, die stop_f ausführt
def stop_dump() =
f = !stop_f
f ()
end
def on_start()
recordingfile := list.hd(get_process_lines("date +#{!filenamepattern}"))
end
# Wav header fixen und ggf. die Aufzeichnung beenden
def on_close(filename)
# es darf wieder aufgenommen werden
is_record_active := false
# if list.assoc("rec_filetype", ini) == 'wav'
# # Korrekten WAV-Header schreiben
# system("qwavheaderdump -F #{filename}")
# Naechsten Dateinamen vormerken
recordingfile := list.hd(get_process_lines("date +#{!filenamepattern}"))
end
# Der input wie oben definiert
def get_input()
output_source
# input.alsa()
end
def get_output()
input = get_input()
d = int_of_string(list.assoc("rec_duration", ini))
if rec_filetype == 'flac' then
log("output file type is FLAC")
output.file(
id="recorder",
%flac(samplerate=44100, channels=2, compression=5, bits_per_sample=16),
perm = 0o664,
on_start=on_start,
!filenamepattern,
on_close=on_close,
reopen_when={ if !instance == 'record' then int_of_float(gettimeofday()/60.) mod 30 == 0 else false end },
input
)
else
# record in WAV
log("output file type is WAV")
output.file(
id="recorder",
%wav(stereo=true, channels=2, samplesize=16, header=true),
perm = 0o664,
on_start=on_start,
!filenamepattern,
on_close=on_close,
reopen_when={ if !instance == 'record' then int_of_float(gettimeofday()/60.) mod d == 0 else false end },
input
)
end
end
# Funktion gibt Auskunft welches File aktuell ist und wieviel Prozent bereits aufgenommen werden
def currecording()
curfile = !recordingfile
if curfile != "" then
percent_done = list.hd(get_process_lines("echo $(($(stat -c%s "^curfile^")/3174777))"))
"#{curfile}, #{percent_done}%"
else
"Nothing is being recorded now"
end
end
#Funktion zum Start der Aufzeichnung
def start_dump() =
log('start dump')
# don't record twice is_record_active
if !is_record_active == false then
is_record_active := true
log('starting to record')
record = get_output()
log('record defined')
# Die Stopfunkton zeigt nun auf die Shutdown-Funktion der aktuellen Source
stop_f := fun () -> begin
log('stop recording')
# Aufnahme sauber beenden
ignore(server.execute('recorder.stop'))
# Source zerstören
source.shutdown(record)
# Variable zurücksetzen
recordingfile := ""
end
else
log("recorder already active")
end
end
# Der Server wird durch 3 Funktionen bereichert
# Der User darf die Aufzeichnung manuell starten
server.register(namespace="record",
description="Start recording.",
usage="start",
"start",
fun (s) -> begin start_dump() "OK" end)
# Der User darf die Aufzeichnung manuell stoppen
server.register(namespace="record",
description="Stop recording.",
usage="stop",
"stop",
fun (s) -> begin stop_dump() "OK" end)
if !instance != 'record' then
# Der User darf einen Dateinamen für die Aufnahme definieren
server.register(namespace="record",
description="Define filename for output.",
usage="setfilename",
"setfilename",
fun (s) -> begin filenamepattern := audiobase^"/"^string_of(s) "OK" end)
end
# Der User kann sich über den Fortschritt der Aufnahme informieren
server.register(namespace="record",
description="Show current file.",
usage="curfile",
"curfile",
fun (s) -> currecording() )
output.dummy(blank(id="serve"))
# define file name pattern
filenamepattern = ref audiobase^"/%Y-%m-%d/%Y-%m-%d-%H-%M.flac"
# Der aktuelle Dateiname für die Aufnahme
recordingfile = ref ""
# bewahre uns davor, dass zweimal gleichzeitig die gleiche Date aufgenommen wird
is_record_active = ref false
#wir definieren eine Referenz für die Stop-Funktion, die aber bisher noch nichts tun kann
stop_f = ref (fun () -> ())
def start_wav_output(recfile, filenamepattern, recorder_number)
duration = int_of_string(list.assoc("rec_"^recorder_number^"_duration", ini))
channels = int_of_string(list.assoc("rec_"^recorder_number^"_channels", ini))
samplesize = int_of_string(list.assoc("rec_"^recorder_number^"_samplesize", ini))
ignore(duration)
ignore(channels)
ignore(samplesize)
# def on_start()
# recfile := list.hd(get_process_lines("date +#{!filenamepattern}"))
# end
# def on_close(filename)
# recordingfile := list.hd(get_process_lines("date +#{!filenamepattern}"))
# end
print(channels)
print(samplesize)
#out_wav = output.file(id="recorder", perm = 0o664, on_start=on_start, on_close=on_close, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 })
#out_wav(%wav(stereo=true, channels=2, samplesize=8, header=true, !filenamepattern, output_source)
#ignore(out_wav)
output.dummy(id="wav_dummy_recording", blank())
# if channels == 2 then
# output.file(id="recorder", %wav(stereo=true, channels=2, samplesize=8, header=true), perm = 0o664, on_start=on_start, on_close=on_close, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 }, !filenamepattern, audio_to_stereo(output_source))
## out_wav(output_source)
# else
# output.file(id="recorder", %wav(stereo=true, channels=1, samplesize=8, header=true), perm = 0o664, on_start=on_start, on_close=on_close, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 }, !filenamepattern, output_source)
# out_wav(output_source)
# end
#if channels == 2 then
# if samplesize < 12 then
# ignore(out_wav(%wav(stereo=true, channels=2, samplesize=8, header=true), !filenamepattern, output_source))
# else
# ignore(out_wav(%wav(stereo=true, channels=2, samplesize=16, header=true), !filenamepattern, output_source))
# end
#else
# if samplesize < 12 then
# ignore(out_wav(%wav(stereo=true, channels=1, samplesize=8, header=true), !filenamepattern, output_source))
# else
# ignore(out_wav(%wav(stereo=true, channels=1, samplesize=16, header=true), !filenamepattern, output_source))
# end
#end
end
def start_flac_output(recorder_number, filenamepattern, duration)
# duration = int_of_string(list.assoc("rec_"^recorder_number^"_samplerate", ini))
# samplerate = list.assoc("rec_"^recorder_number^"_samplerate", ini)
channels = int_of_string(list.assoc("rec_"^recorder_number^"_channels", ini))
# compression = int_of_string(list.assoc("rec_"^recorder_number^"_compression", ini))
# bits_per_sample = int_of_string(list.assoc("rec_"^recorder_number^"_bits_per_sample", ini))
#output.file(id="recorder", %flac(samplerate=44100, channels=1, compression=7, bits_per_sample=32), perm = 0o664, on_start=on_start, !filenamepattern, on_close=on_close, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 }, output_source)
recfile = ref ''
def on_start()
recfile := list.hd(get_process_lines("date +#{filenamepattern}"))
end
def on_close(filename)
recfile := list.hd(get_process_lines("date +#{filenamepattern}"))
end
# dumbass liquidsoap cannot handle one output definition for mono and stereo
output_filesystem_mono = output.file(id="recorder", perm = 0o664, on_start=on_start, on_close=on_close, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 })
output_filesystem_stereo = output.file(id="recorder", perm = 0o664, on_start=on_start, on_close=on_close, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 })
ignore(output_filesystem_mono)
ignore(output_filesystem_stereo)
if channels == 2 then
output.dummy(id="flac_dummy_recording_stereo", blank())
# output_stereof(%flac(samplerate=44100, channels=1, compression=1, bits_per_sample=16), !filenamepattern, output_source)
else
output.dummy(id="flac_dummy_recording_mono", blank())
# output_icecast_monof(%flac(samplerate=44100, channels=1, compression=1, bits_per_sample=16), !filenamepattern, output_source)
end
end
def enable_stop_function(record)
# Die Stopfunkton zeigt nun auf die Shutdown-Funktion der aktuellen Source
stop_f := fun () -> begin
log('stop recording')
# Aufnahme sauber beenden
ignore(server.execute('recorder.stop'))
# Source zerstören
source.shutdown(record)
# Variable zurücksetzen
recordingfile := ""
end
end
def set_recorder_output(rec_filetype, recorder_number)
# flac output
if rec_filetype == 'flac' then
log("output file type is FLAC")
record = start_flac_output(recorder_number)
enable_stop_function(record)
# WAV output
else
log("output file type is WAV")
record = start_wav_output(recorder_number)
enable_stop_function(record)
end
end
# shows current file and how many bytes were written so far
def currecording()
curfile = !recordingfile
if curfile != "" then
bytes_written = list.hd(get_process_lines("echo $(($(stat -c%s "^curfile^")))"))
"#{curfile}, #{bytes_written}B"
else
""
end
end
\ No newline at end of file
def get_icecast_mp3_stream(number)
stream_bitrate = int_of_string(list.assoc("stream_#{number}_bitrate", ini))
stream_mountpoint = list.assoc("stream_#{number}_mountpoint", ini)
stream_host = list.assoc("stream_#{number}_host", ini)
stream_port = int_of_string(list.assoc("stream_#{number}_port", ini))
stream_name = list.assoc("stream_#{number}_name", ini)
stream_url = list.assoc("stream_#{number}_url", ini)
stream_genre = list.assoc("stream_#{number}_genre", ini)
stream_description = list.assoc("stream_#{number}_description", ini)
stream_user = list.assoc("stream_#{number}_user", ini)
stream_password = list.assoc("stream_#{number}_password", ini)
if stream_bitrate == 24 then
icecast_stream = output.icecast(%mp3(bitrate = 24, samplerate = 22050), mount=stream_mountpoint, host=stream_host, port=stream_port, name=stream_name, url=stream_url, genre=stream_genre, description=stream_description, user=stream_user, password=stream_password, icy_metadata="true", fallible=true, buffer(output_source))
clock.assign_new(id="stream_#{number}",
[icecast_stream]
)
icecast_stream
else
icecast_stream = output.icecast(%mp3(samplerate = 44100), mount=stream_mountpoint, host=stream_host, port=stream_port, name=stream_name, url=stream_url, genre=stream_genre, description=stream_description, user=stream_user, password=stream_password, icy_metadata="true", fallible=true, buffer(output_source))
clock.assign_new(id="stream_#{number}",
[icecast_stream]
)
icecast_stream
end
end
def get_icecast_ogg_stream(number)
stream_quality = float_of_string(list.assoc("stream_#{number}_quality", ini))
stream_mountpoint = list.assoc("stream_#{number}_mountpoint", ini)
stream_host = list.assoc("stream_#{number}_host", ini)
stream_port = int_of_string(list.assoc("stream_#{number}_port", ini))
stream_name = list.assoc("stream_#{number}_name", ini)
stream_url = list.assoc("stream_#{number}_url", ini)
stream_genre = list.assoc("stream_#{number}_genre", ini)
stream_description = list.assoc("stream_#{number}_description", ini)
stream_user = list.assoc("stream_#{number}_user", ini)
stream_password = list.assoc("stream_#{number}_password", ini)
if stream_quality >= 0.5 then
icecast_stream = output.icecast(%vorbis(quality = 1.0), mount=stream_mountpoint, host=stream_host, port=stream_port, name=stream_name, url=stream_url, genre=stream_genre, description=stream_description, user=stream_user, password=stream_password, icy_metadata="true", fallible=true, buffer(output_source))
clock.assign_new(id="stream_#{number}",
[icecast_stream]
)
icecast_stream
else
icecast_stream = output.icecast(%vorbis(quality = 0.1), mount=stream_mountpoint, host=stream_host, port=stream_port, name=stream_name, url=stream_url, genre=stream_genre, description=stream_description, user=stream_user, password=stream_password, icy_metadata="true", fallible=true, buffer(output_source))
clock.assign_new(id="stream_#{number}",
[icecast_stream]
)
icecast_stream
end
end
def get_harbour_mp3_stream(number)
stream_bitrate = int_of_string(list.assoc("stream_#{number}_bitrate", ini))
stream_user = list.assoc("stream_#{number}_user", ini)
stream_password = list.assoc("stream_#{number}_password", ini)
stream_port = int_of_string(list.assoc("stream_#{number}_port", ini))
stream_url = list.assoc("stream_#{number}_url", ini)
stream_mountpoint = list.assoc("stream_#{number}_mountpoint", ini)
if stream_bitrate == 24 then
harbourstream = output.harbor(%mp3(bitrate = 24, samplerate = 22050), user=stream_user, password=stream_password, id="stream", port=stream_port, url=stream_url, mount=stream_mountpoint, icy_metadata="true", fallible=true, buffer(output_source))
clock.assign_new(id="stream_#{number}",
[harbourstream]
)
harbourstream
else
harbourstream = output.harbor(%mp3, user=stream_user, password=stream_password, id="stream", port=stream_port, url=stream_url, mount=stream_mountpoint, icy_metadata="true", fallible=true, buffer(output_source))
clock.assign_new(id="stream_#{number}",
[harbourstream]
)
harbourstream
end
end
def get_stream(number)
stream = list.assoc("stream_#{number}", ini)
stream_type = list.assoc("stream_#{number}_type", ini)
stream_format = list.assoc("stream_#{number}_format", ini)
# is stream enabled?
if stream == "y" then
log("activating stream #{number}")
if stream_type == "icecast" then
log("its an icecast stream")
if stream_format == "mp3" then
log("filled with mp3")
get_icecast_mp3_stream(number)
elsif stream_format == "vorbis" then
log("filled with ogg")
get_icecast_ogg_stream(number)
else
output.dummy(id="no_valid_stream_format_DUMMY_ICECAST", blank())
end
elsif stream_type == "harbor" then
log("its an harbor stream")
if stream_format == "mp3" then
get_harbour_mp3_stream(number)
else
output.dummy(id="no_valid_stream_format_DUMMY_HARBOUR", blank())
end
else
output.dummy(id="no_valid_stream_type_DUMMY", blank())
end
else
output.dummy(id="no_stream_enabled_DUMMY", blank())
end
end
def set_streams()
stream_0 = get_stream(0)
#stream_1 = get_stream(1)
#stream_2 = get_stream(2)
#stream_3 = get_stream(3)
#stream_4 = get_stream(4)
end
#
# 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 datetime
import threading
from collections import namedtuple
from modules.communication.mail import AuraMailer
from libraries.exceptions.auraexceptions import MailingException
from libraries.exceptions.auraexceptions import DiskSpaceException
# ------------------------------------------------------------------------------------------ #
class DiskSpaceWatcher(threading.Thread):
liquidsoapcommunicator = None
exit_event = None
config = None
logger = None
mailer = None
sent_a_mail = False
is_critical = False
# ------------------------------------------------------------------------------------------ #
def __init__(self, config, logger, liquidsoapcommunicator):
threading.Thread.__init__(self)
self.liquidsoapcommunicator = liquidsoapcommunicator
self.config = config
self.logger = logger
self.mailer = AuraMailer(self.config)
self.exit_event = threading.Event()
# ------------------------------------------------------------------------------------------ #
def run(self):
# set seconds to wait
try:
seconds_to_wait = int(self.config.get("diskspace_check_interval"))
except:
seconds_to_wait = 600
while not self.exit_event.is_set():
try:
# calc next time
next_time = datetime.datetime.now() + datetime.timedelta(seconds=seconds_to_wait)
# check disk space
self.check_disk_space()
# write to logger
self.logger.info("Diskspace checked! Going to start next time " + str(next_time))
# and wait
self.exit_event.wait(seconds_to_wait)
except BrokenPipeError as e:
self.logger.critical("Cannot check if recorder is running. It seems LiquidSoap is not running. Reason: " + str(e))
# ------------------------------------------------------------------------------------------ #
def stop(self):
self.exit_event.set()
# ------------------------------------------------------------------------------------------ #
def check_disk_space(self):
# check disk space where aure engine is writing to
self.check_recorder_disk_space()
self.check_logging_disk_space()
if self.is_critical:
self.logger.critical("Recorder STOPPED due to LOW diskspace! FIX THIS!!!")
if self.sent_a_mail:
self.logger.warning("Recorder is going stop soon because of not enough diskspace! FIX THIS!")
if not self.is_critical and not self.sent_a_mail:
self.logger.debug("No disk space issues detected.")
self.is_critical = False
self.sent_a_mail = False
# ------------------------------------------------------------------------------------------ #
def check_recorder_disk_space(self):
for i in range(5):
if self.config.get("rec_" + str(i)) == "y":
self.check_recorder_num_disk_space(i)
# ------------------------------------------------------------------------------------------ #
def check_recorder_num_disk_space(self, num):
folder = self.config.get("rec_" + str(num) + "_folder")
try:
self.check_disk_space_of_folder(folder)
# ensure recorder is running
if self.liquidsoapcommunicator.is_liquidsoap_running:
self.liquidsoapcommunicator.recorder_start(num)
else:
self.logger.warning("Cannot enable recorder. Liquidsoap is not running!")
except DiskSpaceException as e:
self.logger.critical(str(e))
# stop recorder when diskspace is critical
if self.liquidsoapcommunicator.is_liquidsoap_running:
self.liquidsoapcommunicator.recorder_stop(num)
else:
self.logger.warning("Cannot stop recorder. Liquidsoap is not running!")
# ------------------------------------------------------------------------------------------ #
def check_logging_disk_space(self):
try:
self.check_disk_space_of_folder(self.config.get("logdir"))
except DiskSpaceException as e:
self.logger.critical(str(e))
# ------------------------------------------------------------------------------------------ #
def check_disk_space_of_folder(self, folder):
warning_value_raw = self.config.get("diskspace_warning_value")
critical_value_raw = self.config.get("diskspace_critical_value")
try:
warning_value = self.parse_diskspace(warning_value_raw)
except ValueError:
warning_value_raw = "2G"
warning_value = self.parse_diskspace(warning_value_raw)
try:
critical_value = self.parse_diskspace(critical_value_raw)
except ValueError:
critical_value_raw = "200M"
critical_value = self.parse_diskspace(critical_value_raw)
usage = namedtuple("usage", "total used free")
diskspace = os.statvfs(folder)
free = diskspace.f_bavail * diskspace.f_frsize
total = diskspace.f_blocks * diskspace.f_frsize
used = (diskspace.f_blocks - diskspace.f_bfree) * diskspace.f_frsize
if free < warning_value:
subj = "Diskspace warning"
msg = "Free space in " + folder + " under " + warning_value_raw + ". " + str(usage(total, used, free))
self.send_mail(subj, msg)
if self.liquidsoapcommunicator.is_liquidsoap_running:
self.liquidsoapcommunicator.recorder_start()
else:
self.logger.warning("Cannot enable recorder. Liquidsoap is not running!")
self.sent_a_mail = True
elif free < critical_value:
subj = "Critical diskspace - Recorder stopped!"
msg = "Free space in " + folder + " under " + critical_value_raw + ". " + str(usage(total, used, free))
self.send_mail(subj, msg)
self.sent_a_mail = True
self.is_critical = True
raise DiskSpaceException("Diskspace in " + folder + " reached critical value!")
# ------------------------------------------------------------------------------------------ #
def send_mail(self, subj, msg):
try:
self.logger.info("Trying to send mail with subject " + subj + " and message " + msg + ".")
self.mailer.send_admin_mail(subj, msg)
except MailingException as e:
self.logger.critical("Cannot send mail with subject " + subj + " and message " + msg + ". Reason: " + str(e))
# ------------------------------------------------------------------------------------------ #
def parse_diskspace(self, value):
if value.endswith("K") or value.endswith("k"):
return int(value[:-1]) * 1024
if value.endswith("M") or value.endswith("m"):
return int(value[:-1]) * 1024 * 1024
if value.endswith("G") or value.endswith("g"):
return int(value[:-1]) * 1024 * 1024 * 1024
if value.endswith("T") or value.endswith("t"):
return int(value[:-1]) * 1024 * 1024 * 1024 * 1024
return int(value)
\ 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 sys
import threading
import json
import queue
import traceback
import urllib
import logging
from mutagen.flac import FLAC
from datetime import datetime, timedelta
from libraries.database.broadcasts import Schedule, Playlist, PlaylistEntry, PlaylistEntryMetaData
from libraries.enum.auraenumerations import ScheduleEntryType
from modules.communication.redis.messenger import RedisMessenger
from modules.scheduling.calender_fetcher import CalendarFetcher
class AuraCalendarService(threading.Thread):
messenger = None
until = ""
playlistdir = ""
xmlplaylist = range(0)
queue = None
config = None
debug = False
_stop_event = None
logger = None
fetched_schedule_data = None
# FIXME is it needed?
#url = dict()
data = dict()
calendar_fetcher = None
"""
Fetching playlist data, write it into the database and notify service
"""
def __init__(self, config, datefrom="", dateto=""):
threading.Thread.__init__(self)
self.config = config
self.messenger = RedisMessenger(config)
self.logger = logging.getLogger("AuraEngine")
self.messenger.set_channel("aura")
self.messenger.set_section("calendar")
self.datefrom = str(datefrom)
self.dateto = dateto
self.queue = queue.Queue()
self._stop_event = threading.Event()
# FIXME is it needed?
# self.__set_url__("api_calendar_url")
# self.__set_url__("api_playlist_url")
self.calendar_fetcher = CalendarFetcher(config)
# ------------------------------------------------------------------------------------------ #
def set_date_from(self, date):
self.datefrom = str(date).replace(" ", "T")
# ------------------------------------------------------------------------------------------ #
def set_date_to(self, date):
self.dateto = str(date).replace(" ", "T")
# ------------------------------------------------------------------------------------------ #
def set_until_time(self, timestring):
self.until = timestring
# ------------------------------------------------------------------------------------------ #
def set_playlist_store(self, path):
self.playlistdir = path
# ------------------------------------------------------------------------------------------ #
def get_duration(self, start, end):
return self.__calc_duration__(start, end)
# ------------------------------------------------------------------------------------------ #
def get_queue(self):
return self.queue
# ------------------------------------------------------------------------------------------ #
# FIXME is get_uri() needed?
# def get_uri(self):
# if not self.playlistdir:
# return False
# if not self.datefrom:
# return False
# if not self.__calc_date_to__():
# return
# hostname = self.get("servername")
# port = self.get("serviceport")
# date_from = self.datefrom[0:16] + ":00"
# date_to = self.dateto[0:16] + ":00"
# uri = "http://" + hostname + ":" + port + "/playlist/" + date_from + "/" + date_to
# return uri
# ------------------------------------------------------------------------------------------ #
def run(self):
"""
Fetch calendar data and store it in the database
"""
try:
fetched_schedule_data = self.calendar_fetcher.fetch()
# if nothing is fetched, return
if not fetched_schedule_data:
self.queue.put("fetching_aborted Nothing fetched")
return
# Check what we've got
self.logger.debug("Schedule data: " + str(fetched_schedule_data))
ret_schedule = []
for schedule in fetched_schedule_data:
# Check schedule for validity
if "start" not in schedule:
self.logger.warning("No 'start' of schedule given. Skipping the schedule: %s " % str(schedule))
continue
if "end" not in schedule:
self.logger.warning("No 'end' of schedule given. Skipping the schedule: %s " % str(schedule))
continue
# Store the schedule
schedule_db = self.store_schedule(schedule)
# Store playlists to play
self.store_playlist(schedule_db, schedule_db.playlist_id, schedule["playlist"])
if schedule_db.schedule_fallback_id:
self.store_playlist(schedule_db, schedule_db.schedule_fallback_id, schedule["schedule_fallback"], 1)
if schedule_db.show_fallback_id:
self.store_playlist(schedule_db, schedule_db.show_fallback_id, schedule["show_fallback"], 2)
if schedule_db.station_fallback_id:
self.store_playlist(schedule_db, schedule_db.station_fallback_id, schedule["station_fallback"], 3)
ret_schedule.append(schedule_db)
# release the mutex
self.queue.put(ret_schedule)
except Exception as e:
# release the mutex
self.logger.warning("Fetching aborted due to: %s" % str(e))
self.queue.put("fetching_aborted " + str(e))
# terminate the thread
return
# ------------------------------------------------------------------------------------------ #
# def drop_the_future(self, time_in_the_future):
# ScheduleEntry.drop_the_future(time_in_the_future)
# Schedule.drop_the_future(time_in_the_future)
# ------------------------------------------------------------------------------------------ #
def store_schedule(self, schedule):
"""
Stores the given schedule to the database.
Args:
schedule (Schedule): The schedule
"""
schedule_db = Schedule.select_show_on_datetime(schedule["start"])
havetoadd = False
if not schedule_db:
self.logger.debug("no schedule with given schedule id in database => create new")
schedule_db = Schedule()
havetoadd = True
# calc duration
duration = self.__calc_duration__(schedule["start"], schedule["end"])
schedule["duration"] = timedelta(seconds=duration).__str__()
schedule_db.show_id = schedule["show_id"]
schedule_db.schedule_id = schedule["schedule_id"]
schedule_db.schedule_start = schedule["start"]
schedule_db.schedule_end = schedule["end"]
schedule_db.show_name = schedule["show_name"]
schedule_db.show_hosts = schedule["show_hosts"]
schedule_db.is_repetition = schedule["is_repetition"]
schedule_db.funding_category = schedule["show_fundingcategory"]
schedule_db.languages = schedule["show_languages"]
schedule_db.type = schedule["show_type"]
schedule_db.category = schedule["show_categories"]
schedule_db.topic = schedule["show_topics"]
schedule_db.musicfocus = schedule["show_musicfocus"]
# if schedule["playlist_id"] is None:
# # FIXME Manually assigned playlist ID.
# schedule["playlist_id"] = 1
schedule_db.playlist_id = schedule["playlist_id"]
schedule_db.schedule_fallback_id = schedule["schedule_fallback_id"]
schedule_db.show_fallback_id = schedule["show_fallback_id"]
schedule_db.station_fallback_id = schedule["station_fallback_id"]
schedule_db.store(add=havetoadd, commit=True)
return schedule_db
def store_playlist(self, schedule_db, playlist_id, fetched_playlist, fallbackplaylist_type=0):
"""
Stores the Playlist to the database.
"""
playlist_db = Playlist.select_playlist_for_schedule(schedule_db.schedule_start, playlist_id)
havetoadd = False
if not playlist_db:
playlist_db = Playlist()
havetoadd = True
self.logger.debug("Storing playlist %d for schedule (%s)" % (playlist_id, str(schedule_db)))
playlist_db.playlist_id = playlist_id
playlist_db.schedule_start = schedule_db.schedule_start
playlist_db.show_name = schedule_db.show_name
playlist_db.fallback_type = fallbackplaylist_type
if "entries" in fetched_playlist:
playlist_db.entry_count = len(fetched_playlist["entries"])
else:
playlist_db.entry_count = 0
playlist_db.store(havetoadd, commit=True)
if playlist_db.entry_count > 0:
self.store_playlist_entries(playlist_db, fetched_playlist)
return playlist_db
def store_playlist_entries(self, playlist_db, fetched_playlist):
"""
Stores the playlist entries to the database.
"""
entry_num = 0
time_marker = playlist_db.start_unix
for entry in fetched_playlist["entries"]:
playlistentry_db = PlaylistEntry.select_playlistentry_for_playlist(playlist_db.artificial_id, entry_num)
havetoadd = False
if not playlistentry_db:
playlistentry_db = PlaylistEntry()
havetoadd = True
# Nano-seconds to seconds
duration = int(float(entry["file"]["duration"]) / 1000000000)
playlistentry_db.duration = duration
playlistentry_db.entry_start = datetime.fromtimestamp(time_marker)
playlistentry_db.artificial_playlist_id = playlist_db.artificial_id
playlistentry_db.entry_num = entry_num
playlistentry_db.uri = entry["uri"]
playlistentry_db.filename = entry["filename"]
playlistentry_db.store(havetoadd, commit=True)
self.store_playlist_entry_metadata(playlistentry_db, entry["file"]["metadata"])
entry_num = entry_num + 1
time_marker += duration
def store_playlist_entry_metadata(self, playlistentry_db, metadata):
"""
Stores the meta-data for a PlaylistEntry.
"""
playlistentrymetadata_db = PlaylistEntryMetaData.select_metadata_for_entry(playlistentry_db.artificial_id)
havetoadd = False
if not playlistentrymetadata_db:
playlistentrymetadata_db = PlaylistEntryMetaData()
havetoadd = True
playlistentrymetadata_db.artificial_entry_id = playlistentry_db.artificial_id
if "artist" not in metadata:
self.logger.warning("Artist not found in metadata for track '%s'. Setting to 'n/a'" % playlistentry_db.filename)
playlistentrymetadata_db.artist = ""
else:
playlistentrymetadata_db.artist = metadata["artist"]
playlistentrymetadata_db.title = metadata["title"]
if "album" in metadata:
playlistentrymetadata_db.album = metadata["album"]
playlistentrymetadata_db.store(havetoadd, commit=True)
# ------------------------------------------------------------------------------------------ #
# FIXME Needed?
# def store_playlist_entry(self, schedule_db, playlist, entry, lastentry, entrynum, fallbackplaylist_type=0):
# schedule_entry_db = Playlist.select_one().select_one_playlist_entry_for_show(schedule_db.schedule_id, fallbackplaylist_type, entrynum)
# havetoadd = False
# if not schedule_entry_db:
# self.logger.debug("no scheduleentry with id " + str(playlist["id"]) + " and pos " + str(entrynum) + " in database => creating a new one")
# # FIXME Needed? No active class declaration
# #schedule_entry_db = ScheduleEntry()
# havetoadd = True
# schedule_entry_db.playlist_id = playlist["id"]
# schedule_entry_db.entry_num = entrynum
# schedule_entry_db.schedule_id = schedule_db.schedule_id
# schedule_entry_db.uri = entry["uri"]
# schedule_entry_db.fallback_type = fallbackplaylist_type
# schedule_entry_db.entry_start = schedule_db.schedule_start + timedelta(seconds=self.get_length(lastentry))
# schedule_entry_db.calc_unix_times()
# if havetoadd:
# schedule_entry_db.define_clean_source()
# self.logger.debug("Storing entries... playlist_id: " + str(playlist["id"]) + " schedule_id: " + str(schedule_db.schedule_id) + " num: " + str(entrynum))
# schedule_entry_db.store(add=havetoadd, commit=True)
# return schedule_entry_db
# ------------------------------------------------------------------------------------------ #
def __calc_date_to__(self):
if self.dateto:
return True
if not self.until:
return False
if not self.datefrom:
return False
date_start = datetime.strptime(self.datefrom.replace("T"," "), "%Y-%m-%d %H:%M:%S")
time_start = date_start.strftime("%H:%M")
day_offset = 1 if (time_start > self.until) else 0
end_date = date_start + timedelta(day_offset)
self.dateto = end_date.strftime("%F") + "T" + self.until
return True
# ------------------------------------------------------------------------------------------ #
@staticmethod
def __calc_duration__(start, end):
"""
Berechnet Zeit in Sekunden aus Differenz zwischen Start und Enddatum
@type start: datetime
@param start: Startzeit
@type end: datetime
@param end: Endzeit
@rtype: int
@return: Zeit in Sekunden
"""
sec1 = int(datetime.strptime(start[0:16].replace(" ","T"),"%Y-%m-%dT%H:%M").strftime("%s"))
sec2 = int(datetime.strptime(end[0:16].replace(" ","T"),"%Y-%m-%dT%H:%M").strftime("%s"))
return (sec2 - sec1)
# ------------------------------------------------------------------------------------------ #
def get_length(self, entry):
if entry is None or entry.type == ScheduleEntryType.STREAM or entry.type == ScheduleEntryType.LIVE_0 or entry.type == ScheduleEntryType.LIVE_1 or entry.type == ScheduleEntryType.LIVE_2 or entry.type == ScheduleEntryType.LIVE_3 or entry.type == ScheduleEntryType.LIVE_4:
return 0
audio_file = FLAC(entry.cleansource)
return audio_file.info.length
# ------------------------------------------------------------------------------------------ #
# FIXME is it needed?
# def __set_url__(self, type):
# #url = self.config.get(type+"url")
# pos = url.find("?")
# if pos > 0:
# self.url[type] = url[0:pos]
# self.data[type] = url[pos:]
# else:
# self.url[type] = url
# ------------------------------------------------------------------------------------------ #
def stop(self):
self._stop_event.set()
import os
import sys
import urllib
import logging
import simplejson
from datetime import datetime, timedelta
#from modules.models.schedule import Schedule
from modules.base.simpleutil import SimpleUtil
class CalendarFetcher:
"""
Fetches the schedules, playlists and playlist entries as JSON
via the API endpoints.
"""
url = dict()
url_parameter = dict()
config = None
logging = None
has_already_fetched = False
fetched_schedule_data = None
# FIXME another crutch because of the missing TANK
used_random_playlist_ids = list()
def __init__(self, config):
"""
Constructor
Args:
config (AuraConfig): Holds the engine configuration
"""
self.config = config
self.logger = logging.getLogger("AuraEngine")
self.__set_url__("api_calendar_url")
self.__set_url__("api_playlist_url")
self.__set_url__("api_show_url")
#
# PUBLIC METHODS
#
def fetch(self):
"""
Retrieve all required data from the API.
"""
# Fetch upcoming schedules from STEERING
try:
self.logger.debug("Fetching schedules from STEERING")
self.fetched_schedule_data = self.__fetch_schedule_data__()
except urllib.error.HTTPError as e:
self.logger.critical("Cannot fetch from " + self.url["api_calendar_url"] + "! Reason: " + str(e))
self.fetched_schedule_data = None
return None
except (urllib.error.URLError, IOError, ValueError) as e:
self.logger.critical("Cannot connect to " + self.url["api_calendar_url"] + "! Reason: " + str(e))
self.fetched_schedule_data = None
return None
# Fetch playlist and fallbacks to the schedules from TANK
try:
self.logger.debug("Fetching playlists from TANK")
self.__fetch_schedule_playlists__()
except urllib.error.HTTPError as e:
self.logger.critical("Cannot fetch from " + self.url["api_playlist_url"] + "! Reason: " + str(e))
self.fetched_schedule_data = None
return None
except (urllib.error.URLError, IOError, ValueError) as e:
self.logger.critical("Cannot connect to " + self.url["api_playlist_url"] + "! Reason: " + str(e))
self.fetched_schedule_data = None
return None
return_data = []
# Gather returndata
try:
for schedule in self.fetched_schedule_data:
# Skip schedule if no start or end is given
if "start" not in schedule:
self.logger.warning("No start of schedule given. Skipping schedule: " + str(schedule))
schedule = None
if "end" not in schedule:
self.logger.warning("No end of schedule given. Skipping schedule: " + str(schedule))
schedule = None
if "playlist" not in schedule:
self.logger.warning("No playlist for schedule given. Skipping schedule: " + str(schedule))
schedule = None
if schedule:
return_data.append(schedule)
except TypeError as e:
self.logger.error("Nothing fetched...")
self.fetched_schedule_data = None
return None
return return_data
#
# PRIVATE METHODS
#
# FIXME Refactor for more transparent API requests.
def __set_url__(self, type):
"""
Initializes URLs and parameters for API calls.
"""
url = self.config.get(type)
pos = url.find("?")
if pos > 0:
self.url[type] = url[0:pos]
self.url_parameter[type] = url[pos:]
else:
self.url[type] = url
def __fetch_schedule_data__(self):
"""
Fetches schedule data from Steering.
Returns:
([Schedule]): An array of schedules
"""
servicetype = "api_calendar_url"
schedule = None
# fetch data from steering
url = self.__build_url__(servicetype)
html_response = self.__fetch_data__(servicetype, url)
# use testdata if response fails or is empty
if not html_response or html_response == b"[]":
self.logger.critical("Got no response from Steering!")
# FIXME move hardcoded test-data to separate testing logic.
html_response = self.get_test_schedules()
# convert to dict
schedule = simplejson.loads(html_response)
# check data
if not schedule:
self.logger.warn("Got no schedule via Playout API (Steering)!")
return None
#self.fetched_schedule_data = self.remove_unnecessary_data(schedule)
return self.remove_unnecessary_data(schedule)
def __fetch_schedule_playlists__(self):
"""
Fetches all playlists including fallback playlists for every schedule.
This method used the class member `fetched_schedule_data`` to iterate
over and extend schedule data.
"""
# store fetched entries => do not have to fetch playlist_id more than once
fetched_entries=[]
try:
for schedule in self.fetched_schedule_data:
# Extend schedule with details of show (e.g. slug)
schedule = self.__fetch_show_details__(schedule)
# Retrieve playlist and the fallback playlists for every schedule.
# If a playlist (like station_fallback) is already fetched, it is not fetched again but reused
schedule["playlist"] = self.__fetch_schedule_playlist__(schedule, "playlist_id", fetched_entries)
schedule["schedule_fallback"] = self.__fetch_schedule_playlist__(schedule, "schedule_fallback_id", fetched_entries)
schedule["show_fallback"] = self.__fetch_schedule_playlist__(schedule, "show_fallback_id", fetched_entries)
schedule["station_fallback"] = self.__fetch_schedule_playlist__(schedule, "station_fallback_id", fetched_entries)
except Exception as e:
self.logger.error("Error: "+str(e))
def __fetch_show_details__(self, schedule):
"""
Fetches details of a show from Steering.
Args:
schedule (Schedule): A schedule holding a valid `show_id`
Returns:
(Schedule): The given schedule with additional show fields set.
"""
servicetype = "api_show_url"
url = self.__build_url__(servicetype, "${ID}", str(schedule["show_id"]))
json_response = self.__fetch_data__(servicetype, url)
show_details = simplejson.loads(json_response)
# Extend "schedules" with details of "show"
schedule["show_slug"] = show_details["slug"]
### ...
### ... Add more properties here, if needed
### ...
return schedule
def __fetch_schedule_playlist__(self, schedule, id_name, fetched_playlists):
"""
Fetches the playlist for a given schedule.
Args:
schedule (Schedule): The schedule to fetch playlists for
id_name (String): The type of playlist to fetch (e.g. normal vs. fallback)
fetched_playlists ([]): Previously fetched playlists to avoid re-fetching
Returns:
([Schedule]): Array of playlists
"""
servicetype = "api_playlist_url"
# fetch playlists from TANK
if not "show_slug" in schedule:
raise ValueError("Missing 'show_slug' for schedule", schedule)
slug = str(schedule["show_slug"])
url = self.__build_url__(servicetype, "${SLUG}", slug)
json_response = self.__fetch_data__(servicetype, url)
# if a playlist is already fetched, do not fetch it again
for playlist in fetched_playlists:
# FIXME schedule["playlist_id"] is always None, review if playlist["id"] is valid
if playlist["id"] == schedule[id_name]:
self.logger.debug("Playlist #" + str(schedule[id_name]) + " already fetched")
return playlist
if self.config.get("use_test_data"):
# FIXME move hardcoded test-data to separate testing logic.
self.logger.warn("Using test-data for fetch-schedule-playlist")
json_response = self.create_test_data(id_name, schedule)
# convert to list
playlists = simplejson.loads(json_response)
pl = None
if "results" in playlists:
# FIXME Currently we use hardcoded playlist and fallback assignments due to issues in Dashboard/Steering/Tank
self.logger.warn("FIXME Currently we use hardcoded playlist and fallback assignments due to issues in Dashboard/Steering/Tank")
i = 0
for playlist in playlists["results"]:
pl = playlist
# FIXME Always use the first playlist, since the schedule.playlist_id is currently not set via Dashboard:
if i == 0 and id_name == "playlist_id":
schedule["playlist_id"] = playlist["id"]
break
# FIXME Currently it's not possible to set & query the fallback for a timeslot/show/station; therefore hardcode it:
elif i == 1 and id_name == "schedule_fallback_id":
schedule["schedule_fallback_id"] = playlist["id"]
break
elif i == 2 and id_name == "show_fallback_id":
schedule["show_fallback_id"] = playlist["id"]
break
elif i == 3 and id_name == "station_fallback_id":
schedule["station_fallback_id"] = playlist["id"]
break
else:
pl = None
i += 1
if pl:
# Note: playlists without entries are allowed -> will trigger fallbacks
if "entries" in pl:
for entry in pl["entries"]:
if entry["uri"].startswith("file"):
entry["filename"] = self.convert_to_filename(entry["uri"])
fetched_playlists.append(pl)
return pl
def convert_to_filename(self, uri):
"""
Converts a file-system URI to an actual, absolute path to the file.
Args:
uri (String): The URI of the file
Returns:
path (String): Absolute file path
"""
e = self.config.get("audiofolder") + "/" + uri[7:] + ".flac"
if not os.path.isfile(e):
self.logger.warning("File %s does not exist!" % e)
return e
def __fetch_data__(self, type, url):
"""
Fetches JSON data for the given URL.
Args:
url (String): The API endpoint to call
Returns:
(Byte[]): An UTF-8 encoded byte object holding the response
"""
html_response = b''
# Send request to the API and read the data
try:
if type not in self.url_parameter:
if self.url[type] == "":
return False
request = urllib.request.Request(url)
else:
request = urllib.request.Request(url, self.url_parameter[type])
response = urllib.request.urlopen(request)
html_response = response.read()
except (urllib.error.URLError, IOError, ValueError) as e:
self.logger.error("Cannot connect to " + self.url[type] +
" (type: " + type + ")! Reason: " + str(e.reason))
#if not self.has_already_fetched: # first fetch
# self.logger.critical("exiting fetch data thread..")
# sys.exit()
self.has_already_fetched = True
return html_response.decode("utf-8")
def __build_url__(self, type, placeholder=None, value=None):
"""
Builds an API request URL using passed placeholder and value.
"""
url = self.url[type]
if placeholder:
url = url.replace(placeholder, value)
# self.logger.info("built URL: "+url)
return url
def remove_unnecessary_data(self, schedule):
"""
Removes all schedules which are not relevant for
further processing.
"""
count_before = len(schedule)
schedule = self.remove_data_more_than_24h_in_the_future(schedule)
schedule = self.remove_data_in_the_past(schedule)
count_after = len(schedule)
self.logger.info("Removed %d unnecessary schedules from response. Entries left: %d" % ((count_before - count_after), count_after))
return schedule
def remove_data_more_than_24h_in_the_future(self, schedules):
"""
Removes entries 24h in the future and 12 hours in the past.
Note: This might influence resuming (in case of a crash)
single schedules which are longer than 12 hours long.
Think e.g. live broadcasts.
"""
items = []
now = SimpleUtil.timestamp()
now_plus_24hours = now + (12*60*60)
now_minus_12hours = now - (12*60*60)
for s in schedules:
start_time = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S")
start_time = SimpleUtil.timestamp(start_time)
if start_time <= now_plus_24hours and start_time >= now_minus_12hours:
items.append(s)
return items
def remove_data_in_the_past(self, schedules):
"""
Removes all schedules from the past, except the one which is
currently playing.
"""
items = []
now = SimpleUtil.timestamp()
for s in schedules:
start_time = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S")
start_time = SimpleUtil.timestamp(start_time)
end_time = datetime.strptime(s["end"], "%Y-%m-%dT%H:%M:%S")
end_time = SimpleUtil.timestamp(end_time)
# Append all elements in the future
if start_time >= now:
items.append(s)
# Append the one which is playing now
elif start_time < now < end_time:
items.append(s)
return items
#
# TESTING
#
def get_test_schedules(self):
html_response = "{}"
# use testdata if wanted
if self.config.get("use_test_data"):
html_response = '[{"id":1,"schedule_id":1,"automation-id":1,"className":"TestData","memo":"TestData","show_fundingcategory":"TestData","start":"' + (datetime.now() + timedelta(hours=0)).strftime('%Y-%m-%dT%H:00:00') + '","end":"' + (datetime.now() + timedelta(hours=1)).strftime('%Y-%m-%dT%H:00:00') + '","show_id":9,"show_name":"TestData: FROzine","show_hosts":"TestData: Sandra Hochholzer, Martina Schweiger","title":"TestData:title","is_repetition":false,"playlist_id":2,"schedule_fallback_id":12,"show_fallback_id":92,"station_fallback_id":1,"rtr_category":"string","comment":"TestData: Kommentar","show_languages":"TestData: Sprachen","show_type":"TestData: Typ","show_categories":"TestData: Kategorie","show_topics":"TestData: Topic","show_musicfocus":"TestData: Fokus"},' \
'{"id":2,"schedule_id":2,"automation-id":1,"className":"TestData","memo":"TestData","show_fundingcategory":"TestData","start":"' + (datetime.now() + timedelta(hours=1)).strftime('%Y-%m-%dT%H:00:00') + '","end":"' + (datetime.now() + timedelta(hours=2)).strftime('%Y-%m-%dT%H:00:00') + '","show_id":10,"show_name":"TestData: FROMat","show_hosts":"TestData: Sandra Hochholzer, Martina Schweiger","title":"TestData:title","is_repetition":false,"playlist_id":4,"schedule_fallback_id":22,"show_fallback_id":102,"station_fallback_id":1,"rtr_category":"TestData: string","comment":"TestData: Kommentar","show_languages":"TestData: Sprachen","show_type":"TestData: Typ","show_categories":"TestData: Kategorie","show_topics":"TestData: Topic","show_musicfocus":"TestData: Fokus"},' \
'{"id":3,"schedule_id":3,"automation-id":1,"className":"TestData","memo":"TestData","show_fundingcategory":"TestData","start":"' + (datetime.now() + timedelta(hours=2)).strftime('%Y-%m-%dT%H:00:00') + '","end":"' + (datetime.now() + timedelta(hours=3)).strftime('%Y-%m-%dT%H:00:00') + '","show_id":11,"show_name":"TestData: Radio für Senioren","show_hosts":"TestData: Sandra Hochholzer, Martina Schweiger","title":"TestData:title","is_repetition":false,"playlist_id":6,"schedule_fallback_id":32,"show_fallback_id":112,"station_fallback_id":1,"rtr_category":"TestData: string","comment":"TestData: Kommentar","show_languages":"TestData: Sprachen","show_type":"TestData: Typ","show_categories":"TestData: Kategorie","show_topics":"TestData: Topic","show_musicfocus":"TestData: Fokus"}]'
self.logger.critical("Using hardcoded Response!")
return html_response
def create_test_data(self, id_name, schedule):
import random
rand_id = random.randint(1, 10000)
while rand_id in self.used_random_playlist_ids:
rand_id = random.randint(1, 10000)
self.used_random_playlist_ids.append(rand_id)
# FIXME move hardcoded test-data to separate testing logic.
# HARDCODED Testdata
if id_name != "playlist_id":
# FALLBACK TESTDATA
if rand_id % 3 == 0: # playlist fallback
json_response = '{"playlist_id":' + str(
rand_id) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"file:///var/audio/fallback/NightmaresOnWax/DJ-Kicks/02 - Only Child - Breakneck.flac"}]}'
elif rand_id % 2 == 0: # stream fallback
json_response = '{"playlist_id":' + str(
rand_id) + ',"entries":[{"source":"http://chill.out.airtime.pro:8000/chill_a"}]}'
else: # pool fallback
json_response = '{"playlist_id":' + str(rand_id) + ',"entries":[{"source":"pool:///liedermacherei"}]}'
schedule[id_name] = rand_id
elif schedule[id_name] == 0 or schedule[id_name] is None:
# this happens when playlist id is not filled out in pv
# json_response = '{"playlist_id": 0}'
if rand_id % 4 == 0: # playlist with two files
json_response = '{"playlist_id":' + str(
rand_id) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"file:///var/audio/fallback/NightmaresOnWax/DJ-Kicks/02 - Only Child - Breakneck.flac"}]}'
elif rand_id % 3 == 0: # playlist with jingle and then linein
json_response = '{"playlist_id":' + str(
rand_id) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"linein://1"}]}'
elif rand_id % 2 == 0: # playlist with jingle and then http stream
json_response = '{"playlist_id":' + str(
rand_id) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"http://chill.out.airtime.pro:8000/chill_a"}]}'
else: # pool playlist
json_response = '{"playlist_id":' + str(rand_id) + ',"entries":[{"source":"pool:///hiphop"}]}'
schedule[id_name] = rand_id
elif schedule[id_name] % 4 == 0: # playlist with two files
json_response = '{"playlist_id":' + str(schedule[id_name]) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"file:///var/audio/fallback/NightmaresOnWax/DJ-Kicks/01 - Type - Slow Process.flac"}]}'
elif schedule[id_name] % 3 == 0: # playlist with jingle and then http stream
json_response = '{"playlist_id":' + str(schedule[id_name]) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"linein://0"}]}'
elif schedule[id_name] % 2 == 0: # playlist with jingle and then linein
json_response = '{"playlist_id":' + str(schedule[id_name]) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"http://stream.fro.at:80/fro-128.ogg"}]}'
else: # pool playlist
json_response = '{"playlist_id":' + str(schedule[id_name]) + ',"entries":[{"source":"pool:///chillout"}]}'
self.logger.info("Using 'randomized' playlist: " + json_response + " for " + id_name[:-3] + " for show " + schedule["show_name"] + " starting @ " + schedule["start"])
return json_response
#
# Aura Engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2020 David Trattnig <david.trattnig@subsquare.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
# Meta
__version__ = '0.0.1'
__license__ = "GNU General Public License (GPL) Version 3"
__version_info__ = (0, 0, 1)
__author__ = 'David Trattnig <david.trattnig@subsquare.at>'
import os, os.path
import logging
import random
import librosa
from accessify import private, protected
from modules.base.simpleutil import SimpleUtil
from modules.communication.mail import AuraMailer
class FallbackManager:
"""
Handles all types of fallbacks in case there is an outage
for the regular radio programme.
Attributes:
config (AuraConfig): The engine configuration
logger (AuraLogger): The logger
mail (AuraMailer): Mail service
scheduler (AuraScheduler): The scheduler
fallback_history (Dict): Holds a 24h history of played, local tracks to avoid re-play
last_fallback (Integer): Timestamp, when the last local file fallback was played
is_processing (Boolean): Flag to avoid race-conditions, as Liquidsoap sends plenty of requests at once
"""
config = None
logger = None
mailer = None
scheduler = None
fallback_history = {}
last_fallback = 0
is_processing = False
def __init__(self, config, logger, scheduler):
"""
Constructor
Args:
config (AuraConfig): Holds the engine configuration
"""
self.config = config
self.mailer = AuraMailer(self.config)
self.scheduler = scheduler
self.logger = logger
#
# PUBLIC METHODS
#
def get_fallback_for(self, fallbackname):
"""
Retrieves a random fallback audio source for any of the types:
- timeslot/schedule
- show
- station
Args:
fallbackname (String): Fallback type
Returns:
(String): Absolute path to the file
"""
file = ""
media_type = "PLAYLIST"
active_schedule, active_playlist = self.scheduler.get_active_playlist()
# Block access to avoid race-conditions
if self.is_processing:
return None
else:
self.is_processing = True
# Get fallback track(s) by fallback-type
if fallbackname == "timeslot":
file = self.get_playlist_items(active_schedule, "schedule_fallback")
elif fallbackname == "show":
file = self.get_playlist_items(active_schedule, "show_fallback")
elif fallbackname == "station":
file = self.get_playlist_items(active_schedule, "station_fallback")
if not file:
media_type = "TRACK"
file = self.get_random_local_track()
if not file:
self.logger.critical("Got no file for station fallback! Playing default test track, to play anything at all.")
file = "../../testing/content/ernie_mayne_sugar.mp3"
media_type = "DEFAULT TRACK"
else:
file = ""
self.logger.critical("Should set next fallback file for " + fallbackname + ", but this fallback is unknown!")
if file:
# Send admin email to notify about the fallback state
if not active_playlist:
active_playlist = "-"
msg = "AURA ENGINE %s FALLBACK DETECTED!\n\n" % fallbackname
msg += "Expected, active Schedule: %s \n" % active_schedule
msg += "Expected, active Playlist: %s \n\n" % active_playlist
msg += "Providing FALLBACK-%s for %s '%s'\n\n" % (media_type, fallbackname, file)
msg += "Please review the schedules or contact your Aura Engine administrator."
self.mailer.send_admin_mail("CRITICAL - Detected fallback for %s" % fallbackname, msg)
self.logger.warn("Providing fallback %s: '%s'. Sent admin email about fallback state" % (media_type, file))
self.is_processing = False
return file
def fallback_has_started(self, artist, title):
"""
Called when a fallback track has actually started playing
"""
self.logger.info("Now playing: fallback track '%s - %s'." % (artist, title))
def get_track_duration(self, file):
"""
Returns the length of the given audio file.
"""
y, sr = librosa.load(file)
duration = librosa.get_duration(y=y, sr=sr)
return duration
#
# PRIVATE METHODS
#
def get_playlist_items(self, schedule, fallback_key):
"""
Retrieves the list of tracks from a playlist defined by `fallback_key`.
"""
playlist_files = ""
if hasattr(schedule, fallback_key):
playlist = getattr(schedule, fallback_key)
if len(playlist) > 0:
playlist = playlist[0]
if playlist and playlist.entries:
for entry in playlist.entries:
playlist_files += entry.filename + "\n"
return playlist_files
def get_random_local_track(self):
"""
Retrieves a random audio track from the local station-fallback directory.
Returns:
(String): Absolute path to an audio file
"""
dir = self.config.fallback_music_folder
files = os.listdir(dir)
audio_files = list(filter(lambda f: self.is_audio_file(dir, f), files))
if not dir or not audio_files:
self.logger.error("Folder 'fallback_music_folder = %s' is empty!" % dir)
return None
# If last played fallback is > 24 hours ago, ignore play history
# This should save used memory if the engine runs for a long time
if self.last_fallback < SimpleUtil.timestamp() - (60*60*24):
self.fallback_history = {}
self.logger.info("Cleared fallback history.")
self.last_fallback = SimpleUtil.timestamp()
# Retrieve files which haven't been played yet
history = set(self.fallback_history.keys())
left_audio_files = list( set(audio_files) - (history) )
self.logger.info("Left fallback audio-files: %d/%d" % (len(left_audio_files), len(audio_files)))
# If nothing left, clear history and start with all files again
if not len(left_audio_files):
self.fallback_history = {}
left_audio_files = audio_files
# Select random track from directory
i = random.randint(0, len(left_audio_files)-1)
file = os.path.join(dir, left_audio_files[i])
# Store track in history, to avoid playing it multiple times
if file:
self.fallback_history[left_audio_files[i]] = SimpleUtil.timestamp()
return file
def is_audio_file(self, dir, file):
"""
Checks if the passed file is an audio file i.e. has a file-extension
known for audio files.
Args:
(File): file: the file object.
Returns:
(Boolean): True, if it's an audio file.
"""
audio_extensions = [".wav", ".flac", ".mp3", ".ogg", ".m4a"]
ext = os.path.splitext(file)[1]
abs_path = os.path.join(dir, file)
if os.path.isfile(abs_path):
if any(ext in s for s in audio_extensions):
return True
return False
\ No newline at end of file
#
# Aura 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/>.
#
# Meta
__version__ = '0.0.1'
__license__ = "GNU General Public License (GPL) Version 3"
__version_info__ = (0, 0, 1)
__author__ = 'Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>'
import time
import json
import datetime
import decimal
import traceback
import sqlalchemy
import logging
import threading
from operator import attrgetter
from modules.base.simpleutil import SimpleUtil
from modules.communication.redis.messenger import RedisMessenger
from modules.scheduling.calendar import AuraCalendarService
from modules.scheduling.fallback_manager import FallbackManager
from libraries.database.broadcasts import AuraDatabaseModel, Schedule, Playlist, PlaylistEntry, PlaylistEntryMetaData, SingleEntry, SingleEntryMetaData, TrackService
from libraries.exceptions.exception_logger import ExceptionLogger
from libraries.enum.auraenumerations import ScheduleEntryType, TimerType, TerminalColors
def alchemyencoder(obj):
"""JSON encoder function for SQLAlchemy special classes."""
if isinstance(obj, datetime.date):
return obj.isoformat()
elif isinstance(obj, decimal.Decimal):
return float(obj)
elif isinstance(obj, sqlalchemy.orm.state.InstanceState):
return ""
elif isinstance(obj, Schedule):
return json.dumps([obj._asdict()], default=alchemyencoder)
else:
return str(obj)
# ------------------------------------------------------------------------------------------ #
class AuraScheduler(ExceptionLogger, threading.Thread):
"""
Aura Scheduler Class
- Retrieves data from Steering and Tank
- Stores and fires events for LiquidSoap
Attributes:
config (AuraConfig): Holds the Engine Configuration
logger: The logger
exit_event(threading.Event): Used to exit the thread if requested
liquidsoapcommunicator: Stores the connection to LiquidSoap
last_successful_fetch (datetime): Stores the last time a fetch from Steering/Tank was successful
programme: The current radio programme to be played as defined in the local engine database
active_entry(Show, Track): This is a Tuple consisting of the currently played `Show` and `Track`
message_timer(Array<threading.Timer>): The message queue of Liquidsoap commands for playlists/tracks to be played
"""
redismessenger = None
job_result = {}
config = None
logger = None
exit_event = None
liquidsoapcommunicator = None
last_successful_fetch = None
programme = None
active_entry = None
message_timer = []
fallback_manager = None
#schedule_entries = None
client = None
def __init__(self, config):
"""
Constructor
Args:
config (AuraConfig): Reads the engine configuration
"""
self.config = config
self.logger = logging.getLogger("AuraEngine")
self.init_error_messages()
self.init_database()
self.fallback_manager = FallbackManager(config, self.logger, self)
self.redismessenger = RedisMessenger(config)
# init threading
threading.Thread.__init__(self)
# init messenger.. FIXME probably not needed anymore
self.redismessenger.set_channel('scheduler')
self.redismessenger.set_section('execjob')
#self.redismessenger.send('Scheduler started', '0000', 'success', 'initApp', None, 'appinternal')
# Create exit event
self.exit_event = threading.Event()
self.start()
def run(self):
"""
Called when thread is started via `start()`. It does following:
1. `self.fetch_new_programme()` periodically from the API depending on the `fetching_frequency` defined in the engine configuration.
2. Loads the latest programme from the database and sets the instance state `self.programme` with current schedules.
3. Queues all playlists of the programm, other than the playlist currently to be played (This is triggered by Liquidsoap itself).
"""
while not self.exit_event.is_set():
seconds_to_wait = int(self.config.get("fetching_frequency"))
next_time = datetime.datetime.now() + datetime.timedelta(seconds=seconds_to_wait)
self.logger.info("Fetch new programmes every %ss. Next fetching in %ss." % (str(seconds_to_wait), str(next_time)))
self.fetch_new_programme()
self.queue_programme()
self.print_message_queue()
self.exit_event.wait(seconds_to_wait)
#
# PUBLIC METHODS
#
def get_active_entry(self):
"""
Retrieves the current `PlaylistEntry` which should be played as per programme.
Publically called via `LiquidSoapCommunicator`.
Important note: This method also updates the state variable `active_entry`.
Returns:
(PlaylistEntry): The track which is (or should) currently being played
"""
now_unix = time.mktime(datetime.datetime.now().timetuple())
# Load programme if necessary
if not self.programme:
self.logger.info("Next track requested: Need to load programme from database first.")
self.load_programme_from_db()
# Check for scheduled playlist
current_schedule, current_playlist = self.get_active_playlist()
if not current_playlist:
if not current_schedule:
self.logger.critical("There's no active playlist nor schedule. It's probably time to play some fallback...")
else:
self.logger.warning("There's no active playlist for a current schedule. Most likely the playlist finished before the end of the schedule.")
return None
time_start = SimpleUtil.fmt_time(current_playlist.start_unix)
time_end = SimpleUtil.fmt_time(current_playlist.start_unix+current_playlist.duration)
time_now = SimpleUtil.fmt_time(now_unix)
self.logger.info("Current Playlist (%d:%d) for show '%s' scheduled to be played at %s until %s (Now: %s)" % (current_playlist.playlist_id, current_playlist.artificial_id, current_playlist.show_name, time_start, time_end, time_now))
# Iterate over playlist entries and store the current one
time_marker = current_playlist.start_unix
current_entry = None
for entry in current_playlist.entries:
self.logger.info(entry)
if entry.start_unix < now_unix < entry.start_unix + entry.duration:
current_entry = entry
break
time_marker += entry.duration
if current_entry:
time_start = SimpleUtil.fmt_time(current_entry.start_unix)
time_end = SimpleUtil.fmt_time(current_entry.start_unix+current_entry.duration)
time_now = SimpleUtil.fmt_time(now_unix)
self.logger.info("Track '%s' is expected playing from %s to %s (Now: %s)" % (current_entry.filename, time_start, time_end, time_now))
if not self.active_entry:
self.logger.warn("Activate track '%s' and [>> FFWD] to current point in time" % (current_entry.filename))
elif self.active_entry.filename != current_entry.filename:
self.logger.critical("--- SOMETHING UNEXPECTED IS PLAYING: %s --vs-- %s" % (self.active_entry.filename, current_entry.filename))
self.active_entry = current_entry
return (current_entry)
else:
# Nothing playing ... fallback will kick-in
self.logger.warning("There's no entry scheduled for playlist '%s'. Is currently -nothing- or a fallback playing?" % str(current_playlist))
return None
# FIXME Review relevance.
def get_act_programme_as_string(self):
"""
Fetches the latest programme and returns it as `String`.
Also used by `ServerRedisAdapter`.
Return:
(String): Programme
Raises:
(Exception): In case the programme cannot be converted to String
"""
programme_as_string = ""
if self.programme is None or len(self.programme) == 0:
self.fetch_new_programme()
try:
programme_as_string = json.dumps([p._asdict() for p in self.programme], default=alchemyencoder)
# FIXME Change to more specific exception
except Exception as e:
self.logger.error("Cannot transform programme into JSON String. Reason: " + str(e))
traceback.print_exc()
return programme_as_string
def print_message_queue(self):
"""
Prints the current message queue i.e. playlists in the queue to be played.
"""
message_queue = ""
messages = sorted(self.message_timer, key=attrgetter('diff'))
if not messages:
self.logger.warning("There's nothing in the Message Queue!")
else:
for msg in messages:
message_queue += str(msg)+"\n"
self.logger.info("Message Queue: \n" + message_queue)
# ------------------------------------------------------------------------------------------ #
def set_next_file_for(self, playlistname):
self.logger.critical("HAVE TO <SET> NEXT FILE FOR: " + playlistname)
self.logger.critical(str(self.get_active_entry()))
if playlistname == "station":
file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
elif playlistname == "timeslot":
file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
elif playlistname == "show":
file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
else:
file = ""
self.logger.critical("Should set next fallback file for " + playlistname + ", but this playlist is unknown!")
self.logger.info("Set next fallback file for " + playlistname + ": " + file)
self.redismessenger.set_next_file_for(playlistname, file)
return file
def get_next_file_for(self, fallbackname):
"""
Evaluates the next **fallback files/playlists** to be played for a given fallback-type.
Valid fallback-types are:
* timeslot
* show
* station
Args:
fallbackname (String): The name of the fallback-type
Returns:
(String): Absolute path to the file to be played as a fallback.
"""
file = self.fallback_manager.get_fallback_for(fallbackname)
if file:
self.logger.info("Got next file '%s' (type: %s)" % (file, fallbackname))
#set_next_file_thread = SetNextFile(fallbackname, show)
#set_next_file_thread.start()
#self.redismessenger.set_next_file_for(playlistname, file)
return file
def update_track_service(self, entry):
"""
Inserts the given, currently playing `PlaylistEntry` to the track-service.
Called by LiquidSoapCommunicator when a new playlist item is going to be activated.
Args:
entry (PlaylistEntry): The item which is currently playing
"""
if not entry.duration:
self.logger.critical("Entry %s has no duration! This may cause malfunction of some engine services." % (str(entry)))
trackservice = TrackService(entry)
trackservice.store(add=True, commit=True)
entry.trackservice_id = trackservice.id
entry.store(add=False, commit=True)
self.logger.info("Stored track-service entry %s" % trackservice)
def adapt_trackservice_title(self, filename, artist, title):
"""
Updates the track-service entry with the info from a fallback track/playlist.
"""
# FIXME
scheduled_entry = self.get_active_entry()
entry = SingleEntry()
meta = SingleEntryMetaData()
# Validate artist and title
if not title:
title = self.config.get("fallback_title_not_available")
# Create Entry
entry.filename = filename
entry.duration = self.fallback_manager.get_track_duration(filename)
if not entry.duration:
self.logger.critical("Entry %s has no duration! This may cause malfunction of some engine services." % (str(entry)))
# Create track service log for local station fallback (type=4)
trackservice = TrackService(entry, 4)
trackservice.store(add=True, commit=True)
entry.store(add=True, commit=True)
# Create Meta
meta.artist = artist
meta.album = ""
meta.title = title
meta.single_entry_id = entry.id
meta.store(add=True, commit=True)
# Reference each other
entry.meta_data_id = meta.id
entry.trackservice_id = trackservice.id
entry.store(add=False, commit=True)
msg = "Track Service active track '%s' updated with fallback source '%s - %s'!" % (scheduled_entry, artist, title)
self.logger.info(msg)
return msg
#
# PRIVATE METHODS
#
def get_active_playlist(self):
"""
Retrieves the schedule and playlist currently to be played as per
schedule. If the current point in time has no playlist assigned,
only the matching schedule is returned.
Returns:
(Schedule, Playlist): The current schedule and playlist tuple.
"""
now_unix = time.mktime(datetime.datetime.now().timetuple())
current_schedule = None
current_playlist = None
# Iterate over all shows and playlists and find the one to be played right now
if self.programme:
for schedule in self.programme:
if schedule.start_unix < now_unix < schedule.end_unix:
current_schedule = schedule
for playlist in schedule.playlist:
if playlist.start_unix < now_unix < playlist.end_unix:
current_playlist = playlist
break
break
return (current_schedule, current_playlist)
def get_next_playlists(self):
"""
Retrieves the playlists to be played after the current one.
Returns:
([Playlist]): The next playlists
"""
now_unix = time.mktime(datetime.datetime.now().timetuple())
next_playlists = []
for schedule in self.programme:
if schedule.end_unix > now_unix:
for playlist in schedule.playlist:
if playlist.start_unix > now_unix:
next_playlists.append(playlist)
return next_playlists
def get_next_entry(self):
"""
Retrieves the playlist entry to be played next.
Returns:
(Playlist): The next playlist track
"""
next_entry = None
current_schedule, current_playlist = self.get_active_playlist()
if not self.active_entry:
self.logger.warn("For some reason there is no active playlist entry set... Fetching it now!")
self.get_active_entry()
if not self.active_entry:
self.logger.warn("Looks like nothing is currently scheduled...")
return None
# Check if there is a next entry in the current playlist
for i, entry in enumerate(self.active_entry.playlist.entries):
if entry is self.active_entry:
if i+1 < len(self.active_entry.playlist.entries):
next_entry = self.active_entry.playlist.entries[i+1]
break
# It might be in the next playlist...
if not next_entry:
next_playlist = None
found_current = False
# Iterate over all schedule and playlists and find the one to be played next
for schedule in self.programme:
for playlist in schedule.playlist:
if playlist is current_playlist:
found_current = True
elif found_current:
next_playlist = playlist
break
if next_playlist:
next_entry = next_playlist.entries[0]
if not next_entry:
self.logger.fatal("There is no next playlist-entry in the programme!")
return next_entry
def queue_programme(self):
"""
Queues the current programme (playlists as per schedule) by creating
timed commands to Liquidsoap to enable the individual tracks of playlists.
"""
active_schedule, active_playlist = self.get_active_playlist()
playlists = self.get_next_playlists()
s = "\n\n PLAYING NOW:"
s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
if active_schedule:
s += "\n│ Playing schedule %s " % active_schedule
if active_playlist:
s += "\n│ └── Playlist %s " % active_playlist
active_entry = active_playlist.current_entry
# Finished entries
for entry in active_playlist.entries:
if active_entry == entry:
break
else:
s += "\n│ └── Entry %s " % SimpleUtil.strike(str(entry))
# Entry currently being played
if active_entry:
s += "\n│ └── Entry %s " % (TerminalColors.GREEN.value+"PLAYING > "+str(active_entry)+TerminalColors.ENDC.value)
# Open entries for current playlist
rest_of_playlist = active_entry.get_next_entries()
s += self.queue_playlist_entries(rest_of_playlist)
else:
s += "\n│ └── %s No Playlist active. Did it finish before the end of the schedule? %s" % (TerminalColors.ORANGE.value, TerminalColors.ENDC.value)
else:
s += "\n│ Nothing. "
s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────"
s += "\n PLAYING NEXT:"
s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
if not playlists:
s += "\n│ Nothing. "
else:
for next_playlist in playlists:
s += "\n│ Queued schedule %s " % next_playlist.schedule
s += "\n│ └── Playlist %s " % next_playlist
if next_playlist.end_unix > next_playlist.schedule.end_unix:
s += "\n│ %s ↑↑↑ Playlist #%s ends after Schedule #%s!%s " % (TerminalColors.RED.value, next_playlist.playlist_id, next_playlist.schedule.schedule_id, TerminalColors.ENDC.value)
s += self.queue_playlist_entries(next_playlist.entries)
s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────\n\n"
self.logger.info(s)
def queue_playlist_entries(self, entries):
"""
Creates Liquidsoap player commands for all playlist items to be executed at the scheduled time.
Args:
entries([PlaylistEntry]): The playlist entries to be scheduled for playout
Returns:
(String): Formatted string to display playlist entries in log
"""
msg = ""
for entry in entries:
# Function to be called by timer
def func(entry):
self.logger.info("=== Executing timed LQS command: activate('%s') ===" % entry)
self.liquidsoapcommunicator.activate(entry)
planned_timer = self.is_something_planned_at_time(entry.start_unix)
now_unix = SimpleUtil.timestamp()
diff = entry.start_unix - now_unix
msg += "\n│ └── Entry %s " % entry
if planned_timer:
# Check if the playlist_id's are different
if planned_timer.entry.entry_id != entry.entry_id:
# If not, stop and remove the old timer, create a new one
self.stop_timer(planned_timer)
entry.switchtimer = self.create_timer(diff, func, [entry], switcher=True)
else:
# If the playlists do not differ => reuse the old timer and do nothing
self.logger.info("Playlist Entry %s is already scheduled - No new timer created!" % entry)
else:
# If nothing is planned at given time, create a new timer
entry.switchtimer = self.create_timer(diff, func, [entry], switcher=True)
return msg
def fetch_new_programme(self):
"""
Fetch the latest programme from `AuraCalendarService` which stores it to the database.
After that, the programme is in turn loaded from the database and stored in `self.programme`.
"""
# Fetch programme from API endpoints
self.logger.info("Trying to fetch new programe from API endpoints...")
acs = AuraCalendarService(self.config)
queue = acs.get_queue()
acs.start() # start fetching thread
response = queue.get() # wait for the end
self.logger.info("... Programme fetch via API done!")
# Reset last successful fetch state
lsf = self.last_successful_fetch
self.last_successful_fetch = None
if response is None:
self.logger.warning("Trying to load programme from Engine Database, because AuraCalendarService returned an empty response.")
elif type(response) is list:
self.programme = response
if self.programme is not None and len(self.programme) > 0:
self.last_successful_fetch = datetime.datetime.now()
self.logger.info("+++ Successfully fetched current programme from API +++")
if len(self.programme) == 0:
self.logger.critical("Programme fetched from Steering/Tank has no entries!")
elif response.startswith("fetching_aborted"):
self.logger.warning("Trying to load programme from database, because fetching was being aborted from AuraCalendarService! Reason: " + response[16:])
else:
self.logger.warning("Trying to load programme from database, because i got an unknown response from AuraCalendarService: " + response)
# Always load latest programme from the database
self.last_successful_fetch = lsf
self.load_programme_from_db()
self.logger.info("Finished loading current programme from database (%s schedules)" % str(len(self.programme)))
for schedule in self.programme:
self.logger.debug("\tSchedule %s with Playlist %s" % (str(schedule), str(schedule.playlist)))
def load_programme_from_db(self):
"""
Loads the programme from Engine's database and enables
them via `self.enable_entries(..)`. After that, the
current message queue is printed to the console.
"""
self.programme = Schedule.select_act_programme()
if not self.programme:
self.logger.critical("Could not load programme from database. We are in big trouble my friend!")
return
# FIXME Still needed?
def enable_entries(self, playlist):
"""
Iterates over all playlist entries and assigs their start time.
Additionally timers for fadings are created.
Args:
playlist(Playlist): The playlist to be scheduled for playout
"""
now_unix = time.mktime(datetime.datetime.now().timetuple())
time_marker = playlist.start_unix
# Old entry for fading out
# FIXME retrieve active entry from previous playlist
old_entry = None
for entry in playlist.entries:
diff=3
entry.start_unix = time_marker
self.enable_timer(diff, entry, old_entry)
old_entry = entry
# time_marker += 1 # FIXME ???
# # Since we also get entries from the past, filter these out
# if time_marker > now_unix:
# # when do we have to start?
# diff = time_marker - now_unix
# diff = 3 # FIXME test
# entry.start_unix = time_marker
# # enable the three timer
# self.enable_timer(diff, entry, old_entry)
# old_entry = entry
# ------------------------------------------------------------------------------------------ #
def enable_timer(self, diff, entry, old_entry):
"""
Create threads to send track-activation messages to LiquidSoap.
Those tracks can be delayed by `diff` seconds.
Args:
diff (Integer): seconds after tracks should be activated
"""
self.logger.critical("ENABLING SWITCHTIMER FOR " + str(entry))
entry.switchtimer = self.add_or_update_timer(diff, self.liquidsoapcommunicator.activate, [entry])
# FIXME Fade In/Out logic: Not sure if that's functional
#self.enable_fading(diff, entry, old_entry)
# ------------------------------------------------------------------------------------------ #
def enable_fading(self, diff, new_entry, old_entry):
# fading times
fade_out_time = float(self.config.get("fade_out_time"))
# enable fading when entry types are different
if old_entry is not None:
if old_entry.type != new_entry.type:
#self.add_or_update_timer(diff, self.liquidsoapcommunicator.fade_out, [old_entry])
old_entry.fadeouttimer = self.create_timer(diff-fade_out_time, self.liquidsoapcommunicator.fade_out, [old_entry], fadeout=True)
self.logger.critical("ENABLING FADEOUTTIMER FOR " + str(old_entry))
# same for fadein except old_entry can be None
else:
#self.add_or_update_timer(diff, self.liquidsoapcommunicator.fade_in, [new_entry])
new_entry.fadeintimer = self.create_timer(diff, self.liquidsoapcommunicator.fade_in, [new_entry], fadein=True)
self.logger.critical("ENABLING FADEINTIMER FOR " + str(new_entry))
# ------------------------------------------------------------------------------------------ #
def add_or_update_timer(self, diff, func, parameters):
timer = None
entry = parameters[0]
planned_timer = self.is_something_planned_at_time(entry.start_unix)
# if something is planned on entry.entry_start
#FIXME
#if 1==0:
if planned_timer:
planned_entry = planned_timer.entry
# check if the playlist_id's are different
if planned_entry.playlist.playlist_id != entry.playlist.playlist_id:
# if not stop the old timer and remove it from the list
self.stop_timer(planned_timer)
# and create a new one
timer = self.create_timer(diff, func, parameters, switcher=True)
# if the playlist id's do not differ => reuse the old timer and do nothing, they are the same
# if nothing is planned at given time, create a new timer
else:
timer = self.create_timer(diff, func, parameters, switcher=True)
if timer is None:
return planned_timer
return timer
# ------------------------------------------------------------------------------------------ #
def stop_timer(self, timer):
# stop timer
timer.cancel()
if timer.entry.fadeintimer is not None:
timer.entry.fadeintimer.cancel()
self.message_timer.remove(timer.entry.fadeintimer)
if timer.entry.fadeouttimer is not None:
timer.entry.fadeouttimer.cancel()
self.message_timer.remove(timer.entry.fadeouttimer)
# and remove it from message queue
self.message_timer.remove(timer)
self.logger.critical("REMOVED TIMER for " + str(timer.entry))
# ------------------------------------------------------------------------------------------ #
def create_timer(self, diff, func, parameters, fadein=False, fadeout=False, switcher=False):
if not fadein and not fadeout and not switcher or fadein and fadeout or fadein and switcher or fadeout and switcher:
raise Exception("You have to call me with either fadein=true, fadeout=true or switcher=True")
t = CallFunctionTimer(diff, func, parameters, fadein, fadeout, switcher)
self.message_timer.append(t)
t.start()
return t
# ------------------------------------------------------------------------------------------ #
def is_something_planned_at_time(self, given_time):
for t in self.message_timer:
if t.entry.start_unix == given_time:
return t
return False
def init_error_messages(self):
"""
Load error messages
"""
error_file = self.config.get("install_dir") + "/errormessages/scheduler_error.js"
f = open(error_file)
self.error_data = json.load(f)
f.close()
def init_database(self):
"""
Initializes the database.
Raises:
sqlalchemy.exc.ProgrammingError: In case the DB model is invalid
"""
if self.config.get("recreate_db") is not None:
AuraDatabaseModel.recreate_db(systemexit=True)
# Check if tables exists, if not create them
try:
Playlist.select_all()
except sqlalchemy.exc.ProgrammingError as e:
errcode = e.orig.args[0]
if errcode == 1146: # Error for no such table
x = AuraDatabaseModel()
x.recreate_db()
else:
raise
def stop(self):
"""
Called when thread is stopped.
"""
self.exit_event.set()
# ------------------------------------------------------------------------------------------ #
# class SetNextFile(threading.Thread):
# fallbackname = None
# show = None
# def __init__(self, fallbackname, show):
# threading.Thread.__init__(self)
# self.fallbackname = fallbackname
# self.show = show
# def run(self):
# if self.fallbackname == "show":
# self.detect_next_file_for(self.show.showfallback)
# elif self.fallbackname == "timeslow":
# self.detect_next_file_for(self.show.timeslotfallback)
# elif self.fallbackname == "station":
# self.detect_next_file_for(self.show.stationfallback)
# def detect_next_file_for(self, playlist):
# return ""
# #if playlist.startswith("pool"):
# # self.find_next_file_in_pool(playlist)
# #def find_next_file_in_pool(self, pool):
# # return ""
# ------------------------------------------------------------------------------------------ #
class CallFunctionTimer(threading.Timer):
logger = None
param = None
entry = None
diff = None
fadein = False
fadeout = False
switcher = False
def __init__(self, diff, func, param, fadein=False, fadeout=False, switcher=False):
self.logger = logging.getLogger("AuraEngine")
self.logger.debug("CallFunctionTimer: Executing LiquidSoap command '%s' in %s seconds..." % (str(func.__name__), str(diff)))
threading.Timer.__init__(self, diff, func, param)
# TODO Review usage of the fading-attributes:
if not fadein and not fadeout and not switcher or fadein and fadeout or fadein and switcher or fadeout and switcher:
raise Exception("You have to create me with either fadein=true, fadeout=true or switcher=True")
self.diff = diff
self.func = func
self.param = param
self.entry = param[0]
self.fadein = fadein
self.fadeout = fadeout
self.switcher = switcher
# ------------------------------------------------------------------------------------------ #
def __str__(self):
if self.fadein:
return "CallFunctionTimer starting in " + str(self.diff) + "s fading in source '" + str(self.entry)
elif self.fadeout:
return "CallFunctionTimer starting in " + str(self.diff) + "s fading out source '" + str(self.entry)
elif self.switcher:
return "CallFunctionTimer starting in " + str(self.diff) + "s switching to source '" + str(self.entry)
else:
return "CORRUPTED CallFunctionTimer around! How can that be?"
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
#
# import json
# import decimal
# import traceback
# import sqlalchemy
# import datetime
# import logging
#
# from flask import request, render_template
#
# from aura import app
# #from libraries.database.broadcasts import TrackService, Schedule, ScheduleEntry
#
#
#
# def alchemyencoder(obj):
# """JSON encoder function for SQLAlchemy special classes."""
# if isinstance(obj, datetime.date):
# return obj.isoformat()
# elif isinstance(obj, decimal.Decimal):
# return float(obj)
# elif isinstance(obj, sqlalchemy.orm.state.InstanceState):
# return ""
# elif isinstance(obj, Schedule):
# return json.dumps([obj._asdict()], default=alchemyencoder)
# else:
# return str(obj)
#
#
# class Routes:
# error = None
# scheduler = None
# messenger = None
# lqs_communicator = None
#
# def __init__(self, scheduler, lqs_communicator, messenger, config):
# self.scheduler = scheduler
# self.messenger = messenger
# self.lqs_communicator = lqs_communicator
#
# # when debug is enabled => logging messages appear twice
# app.run(port=config.get("web_port")) #debug=True)
#
# @staticmethod
# @APP.route('/')
# @APP.route('/index')
# def index():
# return render_template("index.html")
#
# @staticmethod
# @APP.route("/trackservice", methods=["GET"])
# def track_service():
# from_time = request.args.get("from")
# to_time = request.args.get("to")
# last = request.args.get("last")
# now = request.args.get("now")
#
# # nothing set => today's playlist
# if from_time == None and to_time == None and now == None:
# selected_date = datetime.date.today()
# trackservice_entries = [] # TrackService.select_by_day(selected_date)
#
# # from and end time set
# elif from_time is not None and to_time is not None:
# to_time = datetime.datetime.strptime(to_time, "%Y-%m-%d")
# from_time = datetime.datetime.strptime(from_time, "%Y-%m-%d")
# trackservice_entries = [] # TrackService.select_by_range(from_time, to_time)
#
# # now set
# elif now == "":
# datetime.date.today()
# trackservice_entries = [] # TrackService.now_playing()
#
# return render_template("trackservice.html",
# length=len(trackservice_entries),
# trackservice_entries=trackservice_entries,
# selected_date=selected_date)
#
# @staticmethod
# @APP.route("/test")
# def test():
# return render_template("index2.html")
#
# @staticmethod
# @APP.route("/login")
# def login():
# return "login"
# #return render_template("index.html")
#
# @staticmethod
# @APP.route("/logout")
# def logout():
# #session.pop("logged_in", None)
# return "logout"
# #return render_template("index.html")
#
# @staticmethod
# @APP.route("/api/v1/trackservice/<selected_date>", methods=["GET"])
# def api_trackservice(selected_date):
# try:
# # convert date
# selected_date = datetime.datetime.strptime(selected_date, "%Y-%m-%d").date()
# # select from database
# tracks_on_selected_date = [] # TrackService.select_by_day(selected_date)
# # return as json
# return json.dumps([tracks._asdict() for tracks in tracks_on_selected_date], default=alchemyencoder)
# except Exception as e:
# import traceback
# traceback.print_exc()
#
# error = "Cannot transform programme into JSON String. Reason: " + str(e)
#
# logger = logging.getLogger("AuraEngine")
# logger.error(error)
#
# return json.dumps({"Error": error})
#
# @staticmethod
# @APP.route("/api/v1/soundserver_state", methods=["GET"])
# def soundserver_settings():
# logger = logging.getLogger("AuraEngine")
# logger.critical("soundserver_state removed!")
# #from modules.communication.liquidsoap.communicator import LiquidSoapCommunicator
# #from modules.base.config import ConfigReader
#
# try:
# #cr = ConfigReader()
# #cr.load_config()
# #lqs = LiquidSoapCommunicator(cr)
# return "check removed!" #lqs.auraengine_state()
# except Exception as e:
# error = "Unable to fetch state from Liquidsoap. Is Soundserver running? Reason: " + str(e)
# logger = logging.getLogger("AuraEngine")
# logger.error(error)
# return json.dumps({"Error": error})
#
#
# @staticmethod
# @APP.route("/api/v1/trackservice/", methods=["GET"])
# def api_trackservice_now():
# return json.dumps({'reached': True})
#
# @staticmethod
# @APP.route("/api/v1/upcoming/", methods=["GET"])
# def api_clock():
# servertime = datetime.datetime.now()
# # get upcoming tracks
# upcoming = ScheduleEntry.select_upcoming()
# # convert to json string
# upcoming_as_json = json.dumps([tracks._asdict() for tracks in upcoming], default=alchemyencoder)
# # add servertime and return it
# return upcoming_as_json.replace('[{', '[{"servertime":'+str(servertime)+"},{", 1)
<!DOCTYPE html>
<html lang="de">
<head>
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/lib/bootstrap.min.css') }}" type="text/css">
<link href="http://fonts.googleapis.com/css?family=Ubuntu:400,300italic" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/lib/jquery-ui.min.css') }}" type="text/css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block customcss %}{% endblock %}
<title>Comba - {% block title %}{% endblock %}</title>
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div class="modal-body">
<form id="modalSearchForm" classs="form-horizontal" method="POST" action="/search/modal/{{ orig_id }}">
<div class="form-group">
<label class="col-sm-2 control-label" for="search">{{ _('Text/Name') }}:</label>
<div class="col-sm-10">
<input class="form-control" id="search" name="search" type="text" value="{{ query.search }}" />
</div>
</div>
<div class="clearfix"></div>
<div class="col-sm-12 form-group"><h4>{{ _('Date Search') }}</h4></div>
<div class="form-group">
<div>
<label class="col-sm-2 control-label" for="from">{{ _('from') }}:</label>
<div class="col-sm-4">
<input class="datepicker form-control" id="from" placeholder="2014-01-01" name="from" type="text" value="{{ query.from }}" />
</div>
<label class="col-sm-2 control-label" for="to">{{ _('to') }}:</label>
<div class="col-sm-4">
<input class="datepicker form-control" id="to" placeholder="2014-12-31" name="to" type="text" value="{{ query.to }}" />
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="col-sm-offset-2 col-sm-10">
<br />
<div class="btn-toolbar" role="toolbar">
<div class="btn-group">
<button class="btn btn-default" type="submit" value="fo"> <span class="glyphicon glyphicon-search"></span> {{ _('Search') }}</button>
<button id="reset_button" class="btn btn-default" type="button" value="reset"> <span class="glyphicon glyphicon-remove"> </span>{{ _('Reset') }}</button>
</div>
</div>
</div>
<div class="clearfix"></div>
<br />
<uL class="list-group">
{% for event in eventlist %}
<li id="li-{{ event.id }}" class="list-group-item">
<div class="col-md-6">
<h3><a style="cursor:hand;cursor:pointer" data-origid="{{ orig_id }}" data-eventid="{{ event.id }}" class="overwrite-close-btn">{{ event.title }}</a></h3>
<div>{{ event.start | formatdate }} - {{ event.end | formatdate }}</div>
<div>{% if event.rerun %}{{ _('Repetition of') }} {{ event.replay_of_datetime | formatdate }}{% endif %}</div>
<div>{{ event.subject }}</div>
</div>
<div class="clearfix"></div>
</li>
{% endfor %}
</uL>
</form>
</div>
<script src="{{ url_for('static', filename='js/lib/jquery-1.10.2.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/bootstrap.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/jquery-ui.min.js') }}"></script>
<script>
function overwriteBroadcast(orig_id, replace_id) {
$('#myModal').modal('hide')
window.location.reload(true)
}
$(function() {
$('.overwrite-close-btn').click(function(e) {
e.preventDefault();
var replace_id = $(this).attr('data-eventid');
var orig_id = $(this).attr('data-origid');
console.log(orig_id + " mit " + replace_id + "ueberschreiben")
jQuery.ajax ({
url: '{{ url_for("search") }}/overwrite/' + orig_id + '/' + replace_id,
cache: false,
complete: function (data) {
window.parent.closeModal();
}
});
});
$('#reset_button').click(function(e){
$('#from').val("")
$('#to').val("")
$('#search').val("")
$('#searchForm').submit()
});
$('.pagination .link').click(function(ev){
ev.preventDefault();
if (!$(this).attr('data-page')) {
return;
}
$('#form-page').val($(this).attr('data-page'));
$('#searchForm').submit();
});
$( ".datepicker" ).datepicker({dateFormat: "yy-mm-dd"});
});
</script>
</body>
\ No newline at end of file
{% extends "layout.html" %}
{% block title %}Monitor{% endblock %}
{% block customcss %}<link rel="stylesheet" href="{{ url_for('static', filename='css/monitor.css') }}" type="text/css">{% endblock %}
{% block pagetitle %}{{ _('Monitor') }}{% endblock %}
{% block body %}
<ul class="nav nav-tabs" role="tablist">
<li><a href="/" role="tab">Home</a></li>
<li class="active"><a href="#system-tab" id="getsystemdata" role="tab" data-toggle="tab">{{ _('System') }}</a></li>
<li><a href="#scheduler-tab" id="getscheduler" role="tab" data-toggle="tab">{{ _('Scheduler') }}</a></li>
<li><a href="#controller-tab" id="getcontroller" role="tab" data-toggle="tab">{{ _('Controller') }}</a></li>
<li><a href="#data-tab" id="getalldata" role="tab" data-toggle="tab">{{ _('Channels') }}</a></li>
<li><a href="#schedulerdata-tab" id="getschedulerdata" role="tab" data-toggle="tab">{{ _('Scheduler Jobs') }}</a></li>
<li><a href="#streaming-tab" id="getstreamingdata" role="tab" data-toggle="tab">{{ _('Streaming') }}</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane fade in active" id="system-tab">
<div id="system-data">
{% include 'sysinfo.html' %}
</div>
</div>
<div class="tab-pane fade" id="scheduler-tab">
<div id="scheduler"></div>
</div>
<div class="tab-pane fade" id="controller-tab">
<div id="controller"></div>
</div>
<div class="tab-pane fade" id="data-tab">
<div id="controller-data"></div>
</div>
<div class="tab-pane fade" id="schedulerdata-tab">
<div id="scheduler-data"></div>
</div>
<div class="tab-pane fade" id="streaming-tab">
<div id="streaming-data"></div>
</div>
</div>
{% endblock %}
{% block customjs %}
<script src="{{ url_for('static', filename='js/lib/jquery-1.10.2.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/bootstrap.min.js') }}"></script>
<script>
function formatJobs(data, app) {
//var html = "<ul class=\"showcase\">";
var html = "";
$.each(data, function( index, job ) {
console.info(job)
var level = job['level'] ? job['level'] : '';
var code = job['code'] ? job['code'] : '';
html = html + '<div class="task">';
if (job['microtime']) {
var timeStr = new Date(parseFloat(job['microtime']) * 1000).toLocaleString();
html = html + '<i>Zeit: ' + timeStr + '</i>'
}
html = html + '<h3><task-title>'+ app + '</task-title> Task: ' + job['job'] +'</h3>';
html = html + '<div class="description"><strong>Message:</strong> '+job['message'] +'</div>';
if (job['level'] == 'info' || job['level'] == 'success') {
var bclass = 'bg-' + job['level'];
var erg = 'wurde mit Erfolg abgeschlossen'
}
if (job['level'] == 'warning' ) {
var bclass = 'bg-'+ job['level'];
var erg = 'gab Warnumeldung aus. Status nicht kritisch.'
}
if (job['level'] == 'error' ) {
var bclass = 'bg-'+ job['level'];
var erg = 'gab Fehlermeldung aus. Fehler muss behoben werden.'
}
if (job['level'] == 'fatal' ) {
var bclass = 'bg-danger';
var erg = 'ist mit kritischem Fehler abgebrochen. Fehler muss dringend behoben werden.'
}
html = html + '<div class="show-job '+bclass+'">'
html = html + '<div class="result"><strong>Resultat:</strong> Der Task <em>"'+ job['job'] + '"</em> ' + erg + ' (Errorcode #' + code +')</div>';
console.info(job['value']);
if (job['value'] && (typeof job['value']=== 'object')) {
var info = job['value']
html = html + '<div class="details"><h4>Details:</h4><ul class="details">';
$.each(job['value'], function( key, val) {
html = html + '<li><strong>'+ key +':</strong> '+ val +'</li>';
});
html = html + '</ul></div>';
}
html = html + '</div></div>'
});
//html = html + "</ul>";
return html;
}
function showlogs(app) {
var url='{{ url_for("monitor") }}' + '/events/' + app
jQuery.ajax ({
url: url,
cache: false,
success: function (data) {
jQuery('#' + app).html(formatJobs(data, app));
},
error: function () {
jQuery('#' + app).html('<strong>Error</strong>');
}
});
}
jQuery('#getscheduler').click(function() {
showlogs('scheduler')
});
jQuery('#getcontroller').click(function() {
showlogs('controller')
});
jQuery('#getalldata').click(function() {
jQuery.ajax ({
url: '{{ url_for("monitor") }}' + '/channels',
cache: false,
success: function (response) {
jQuery('#controller-data').html(response.data);
},
error: function () {
jQuery('#controller-data').html('<strong>Error</strong>');
}
});
});
jQuery('#getschedulerdata').click(function() {
jQuery.ajax ({
url: '{{ url_for("monitor") }}' + '/scheduler',
cache: false,
success: function (response) {
jQuery('#scheduler-data').html(response.data);
},
error: function () {
jQuery('#scheduler-data').html('<strong>Error</strong>');
}
});
});
jQuery('#getsystemdata').click(function() {
jQuery.ajax ({
url: '{{ url_for("monitor") }}' + '/sysinfo',
cache: false,
success: function (response) {
jQuery('#system-data').html(response.data);
},
error: function () {
jQuery('#system-data').html('<strong>Error</strong>');
}
});
});
jQuery('#getstreamingdata').click(function() {
jQuery.ajax ({
url: '{{ url_for("monitor") }}' + '/stream',
cache: false,
success: function (response) {
jQuery('#streaming-data').html(response.data);
},
error: function () {
jQuery('#streaming-data').html('<strong>Error</strong>');
}
});
});
</script>
{% endblock %}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
{% for track in tracklist %}
<track>
<title>{{ track.title }}</title>
<record_at>{{ track.record_at }}</record_at>
<length>{{ track.length }}</length>
<location>{{ track.location }}</location>
<time>{{ track.time }}</time>
<start>{{ track.start }}</start>
<end>{{ track.end }}</end>
<show_at>{{ track.show_at }}</show_at>
<station_name>{{ track.station_name }}</station_name>
<station_id>{{ track.station_id }}</station_id>
<programme_id >{{ track.programme_id }}</programme_id >
</track>
{% endfor %}
</trackList>
</playlist>
\ No newline at end of file
{% extends "layout.html" %}
{% block customjs %}{% endblock %}
{% block title %}{% endblock %}
{% block pagetitle %}{{ _('Preproduction Title')}}{% endblock %}
{% block body %}
<div class="well">
<div class="pull-right"><a href="{{ url_for('search') }}#li-{{ event.id }}" class="btn btn-default">{{ _('Cancel')
}}</a></div>
<div>
<h3>{{ event.title }}</h3>
<div>{{ event.start }} - {{ event.end }}</div>
<div>{% if event.rerun %}{{ _('Repetition of') }} {{ event.replay_of_datetime }}{% endif %}</div>
<div>
<h4>{{ _('Upload Preproduction')}}</h4>
<form class="form-inline" method=POST enctype=multipart/form-data action="{{ url_for('preprod_upload') }}">
<div class="form group">
<input type="hidden" name="returnid" value="{{ event.id }}"/>
<div class="form-control"><input type="file" name="audio"/></div>
<input class="form-control input-sm" type="submit" name="submit" value="{{ _('Upload')}}"/>
<br />
</div>
</form>
<br />
<h4>{{ _('Download Remote Url')}}</h4>
<form class="form-inline" method=POST enctype=multipart/form-data action="{{ url_for('preprod_download_url') }}">
<div class="form group">
<input type="hidden" name="returnid" value="{{ event.id }}"/>
<input class="form-control" id="audiourl" type="text" name="audiourl"/> <button type="submit" class="btn btn-default">{{ _('Download')}}</button>
<br />
</div>
</form>
{% if message %}
<div class="center-block text-danger">{{ message }}</div>
{% endif %}
<div class="clearfix"><br /></div>
</div>
<div>
<ul class="list-group">
{% for override in overrides %}
<li class="list-group-item">{{ override.location|basename }} ({{ override.ordering }})
<div class="pull-right btn-toolbar" role="toolbar">
<div class="btn-group">
{% if not loop.last %}
<a href="{{ url_for('preprod_order', preprod_id=override.id, dir='down') }}" class="btn btn-default btn-xs" type="submit" value="fo"> <span style="color:green" class="glyphicon glyphicon-chevron-down"></span> </a>
{% endif %}
{% if not loop.first %}
<a href="{{ url_for('preprod_order', preprod_id=override.id, dir='up') }}" class="btn btn-default btn-xs" type="submit" value="fo"> <span style="color:green" class="glyphicon glyphicon-chevron-up"></span> </a>
{% endif %}
</div>
<div class="btn-group">
<a href="{{ url_for('preprod_delete', event_id=event.id, preprod_id=override.id) }}" id="reset_button" class="btn btn-default btn-xs" type="button" value="reset"> <span style="color:red" class="glyphicon glyphicon-remove"> </span></a>
</div>
</div>
</li>
{% endfor %}
</ul>
<div class="progress">
<div class="progress-bar progress-bar-success" style="width: {{ green }}%">
<span>{{ procent }}%</span>
</div>
<div class="progress-bar progress-bar-danger" style="width: {{ red }}%"></div>
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
{% endblock %}
\ No newline at end of file