Skip to content
Snippets Groups Projects

Compare revisions

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

Source

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

Target

Select target project
  • aura/engine
  • hermannschwaerzler/engine
  • sumpfralle/aura-engine
3 results
Select Git revision
  • 122-synchronized-ci
  • feat-use-docker-main-tag
  • fix-aura-sysuser
  • fix-broken-pipe-153
  • fix-docker-release
  • fix-push-latest-with-tag
  • fix-streamchannel-retries
  • gitlab-templates
  • improve-test-coverage-137
  • improve-test-coverage-143
  • main
  • orm-less-scheduling
  • remove-mailer
  • update-changelog-alpha3
  • virtual-timeslots-131
  • 1.0.0-alpha1
  • 1.0.0-alpha2
  • 1.0.0-alpha3
  • 1.0.0-alpha4
  • 1.0.0-alpha5
20 results
Show changes
Showing
with 2613 additions and 0 deletions
# 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.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 simplejson
import queue
import traceback
import urllib
import logging
from mutagen.flac import FLAC
from datetime import datetime, timedelta
from libraries.database.broadcasts import Schedule, ScheduleEntry
from libraries.enum.auraenumerations import ScheduleEntryType
from modules.communication.redis.messenger import RedisMessenger
class AuraCalendarService(threading.Thread):
messenger = RedisMessenger()
until = ""
playlistdir = ""
xmlplaylist = range(0)
has_already_fetched = False
queue = None
config = None
debug = False
_stop_event = None
logger = None
url = dict()
data = dict()
"""
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.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()
self.__set_url__("calendar")
self.__set_url__("importer")
# ------------------------------------------------------------------------------------------ #
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
# ------------------------------------------------------------------------------------------ #
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:
# fetch upcoming schedules from STEERING
self.logger.debug("Fetching schedules from STEERING")
self.__fetch_schedule_data__()
# fetch playlist and fallbacks to the schedules from TANK
self.logger.debug("Fetching playlists from TANK")
self.__fetch_schedule_entry_data__()
for schedule in self.fetched_schedule_data:
if "start" not in schedule:
self.logger.warning("No start of schedule given. skipping the schedule: "+str(schedule))
continue
if "end" not in schedule:
self.logger.warning("No end of schedule given. skipping the schedule: "+str(schedule))
continue
# store the schedule
schedule_db = self.store_schedule(schedule)
# store playlists to play
self.store_schedule_playlist(schedule_db, schedule, "playlist")
self.store_schedule_playlist(schedule_db, schedule, "schedule_fallback", 1)
self.store_schedule_playlist(schedule_db, schedule, "show_fallback", 2)
self.store_schedule_playlist(schedule_db, schedule, "station_fallback", 3)
# release the mutex
self.queue.put(schedule) #"fetching_finished")
except Exception as e:
self.queue.put("fetching_aborted " + str(e))
# terminate the thread
return
# ------------------------------------------------------------------------------------------ #
def store_schedule(self, schedule):
schedule_db = Schedule.query.filter(Schedule.schedule_id == schedule["schedule_id"]).first()
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"]
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_schedule_playlist(self, schedule_db, schedule, playlistname, fallbackplaylist_type=0):
playlist = schedule[playlistname]
info = "Schedule playlist (" + playlistname + ") for " + schedule_db.show_name + " stored"
warning = "No scheduleentries for playlist #" + str(playlist['playlist_id']) + " in schedule #" + str(schedule_db.schedule_id) + " found"
entrynum = 0
if "entries" in playlist:
lastentry = None
for entry in playlist["entries"]:
lastentry = self.store_playlist_entry(schedule_db, playlist, entry, lastentry, entrynum, fallbackplaylist_type)
entrynum = entrynum + 1
if lastentry is None:
self.logger.warning(warning)
else:
self.logger.info(info)
else:
self.logger.warning(warning)
# ------------------------------------------------------------------------------------------ #
def store_playlist_entry(self, schedule_db, playlist, entry, lastentry, entrynum, fallbackplaylist_type=0):
schedule_entry_db = ScheduleEntry.select_one(playlist["playlist_id"], entrynum)
havetoadd = False
if not schedule_entry_db:
self.logger.debug("no scheduleentry with id " + str(playlist["playlist_id"]) + " and pos " + str(entrynum) + " in database => creating a new one")
schedule_entry_db = ScheduleEntry()
havetoadd = True
schedule_entry_db.playlist_id = playlist["playlist_id"]
schedule_entry_db.entry_num = entrynum
schedule_entry_db.schedule_id = schedule_db.schedule_id
schedule_entry_db.source = entry["source"]
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()
# schedule_entry_db.define_clean_source()
self.logger.debug("Storing entries... playlist_id: " + str(playlist["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 __fetch_schedule_entry_data__(self):
# store fetched entries => do not have to fetch playlist_id more than once
fetched_entries=[]
try:
for schedule in self.fetched_schedule_data:
# retrieve playlist and the fallbacks for every schedule
# if a playlist is already fetched, it is not fetched again
schedule["playlist"] = self.__fetch_schedule_entries__(schedule, "playlist_id", fetched_entries)
schedule["schedule_fallback"] = self.__fetch_schedule_entries__(schedule, "schedule_fallback_id", fetched_entries)
schedule["show_fallback"] = self.__fetch_schedule_entries__(schedule, "show_fallback_id", fetched_entries)
schedule["station_fallback"] = self.__fetch_schedule_entries__(schedule, "station_fallback_id", fetched_entries)
self.logger.info(str(schedule))
except Exception as e:
self.logger.error(str(e))
# ------------------------------------------------------------------------------------------ #
def __fetch_schedule_entries__(self, schedule, id_name, fetched_schedule_entries):
servicetype = "importer"
use_testdata = False
json_response = self.__fetch_data__(servicetype)
if not json_response:
use_testdata = True
for entry in fetched_schedule_entries:
if entry["playlist_id"] == schedule[id_name]:
self.logger.debug("playlist #" + str(schedule[id_name]) + " already fetched")
return entry
if use_testdata:
# HARDCODED Testdata
if 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}'
import random
rand_id = random.randint(1,100)
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"}]}'
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"}]}'
if schedule[id_name] == 0 or schedule[id_name] is None:
self.logger.info("Using 'randomized' playlist: " + json_response + " for " + id_name[:-3] + " for show " + schedule["show_name"] + " starting @ " + schedule["start"])
else:
self.logger.info("Using hardcoded playlist: " + json_response + " for " + id_name[:-3] + " for show " + schedule["show_name"] + " starting @ " + schedule["start"])
try:
schedule_entries = simplejson.loads(json_response)
except Exception as e:
self.logger.critical("Cannot fetch schedule entries from importer")
sys.exit()
if "entries" in schedule_entries:
for entry in schedule_entries["entries"]:
if entry["source"].startswith("file"):
e = entry["source"][7:] # filter file:// out
if not os.path.isfile(e):
self.logger.warning("File", e, "does not exist!")
fetched_schedule_entries.append(schedule_entries)
return schedule_entries
# ------------------------------------------------------------------------------------------ #
def __fetch_schedule_data__(self):
servicetype = "calendar"
use_testdata = False
html_response = self.__fetch_data__(servicetype)
if not html_response or html_response == b"[]":
self.logger.debug("Got no response: Using testdata")
use_testdata = True
# if an error occours => use testdata
if use_testdata:
html_response = '[{"schedule_id":1,"start":"' + (datetime.now() + timedelta(hours=0)).strftime('%Y-%m-%d %H:00:00') + '","end":"' + (datetime.now() + timedelta(hours=1)).strftime('%Y-%m-%d %H:00:00') + '","show_id":9,"show_name":"FROzine","show_hosts":"Sandra Hochholzer, Martina Schweiger","is_repetition":false,"playlist_id":2,"schedule_fallback_id":12,"show_fallback_id":92,"station_fallback_id":1,"rtr_category":"string","comment":"Kommentar","languages":"Sprachen","type":"Typ","category":"Kategorie","topic":"Topic","musicfocus":"Fokus"},{"schedule_id":2,"schedule_start":"' + (datetime.now()+timedelta(hours=1)).strftime('%Y-%m-%d %H:00:00') + '","schedule_end":"' + (datetime.now()+timedelta(hours=2)).strftime('%Y-%m-%d %H:00:00') + '","show_id":10,"show_name":"FROMat","show_hosts":"Sandra Hochholzer, Martina Schweiger","is_repetition":false,"playlist_id":4,"schedule_fallback_id":22,"show_fallback_id":102,"station_fallback_id":1,"rtr_category":"string","comment":"Kommentar","languages":"Sprachen","type":"Typ","category":"Kategorie","topic":"Topic","musicfocus":"Fokus"},{"schedule_id":3,"schedule_start":"' + (datetime.now()+timedelta(hours=2)).strftime('%Y-%m-%d %H:00:00') + '","schedule_end":"' + (datetime.now() + timedelta(hours=3)).strftime('%Y-%m-%d %H:00:00') + '","show_id":11,"show_name":"Radio für Senioren","show_hosts":"Sandra Hochholzer, Martina Schweiger","is_repetition":false,"playlist_id":6,"schedule_fallback_id":32,"show_fallback_id":112,"station_fallback_id":1,"rtr_category":"string","comment":"Kommentar","languages":"Sprachen","type":"Typ","category":"Kategorie","topic":"Topic","musicfocus":"Fokus"}]'
try:
schedule_from_pv = simplejson.loads(html_response)
except Exception as e:
self.logger.critical("Cannot fetch schedule entries from PV")
sys.exit()
# check data
self.logger.critical("Hardcoded Response && no JSON data checks. I believe what i get here")
d = self.remove_data_more_than_24h_in_the_future(schedule_from_pv)
self.fetched_schedule_data = self.remove_data_in_the_past(d)
return self.fetched_schedule_data
# ------------------------------------------------------------------------------------------ #
def remove_data_more_than_24h_in_the_future(self, schedule_from_pv):
act_list = []
now = datetime.now()
now_plus_24hours = now + timedelta(hours=24)
for s in schedule_from_pv:
date_start = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S")
# append only elements which are close enough to now
if date_start <= now_plus_24hours and date_start >= now - timedelta(hours=1):
act_list.append(s)
return act_list
# ------------------------------------------------------------------------------------------ #
def remove_data_in_the_past(self, schedule_from_pv):
act_list = []
now = datetime.now()
for index,curr in enumerate(schedule_from_pv[:-1]):
date_start = datetime.strptime(curr["start"], "%Y-%m-%dT%H:%M:%S")
date_next_start = datetime.strptime(schedule_from_pv[index+1]["start"], "%Y-%m-%dT%H:%M:%S")
# append all elements in the future
if date_start >= now:
act_list.append(curr)
# append the one which is now playing
if date_start <= now and date_next_start >= now:
act_list.append(curr)
return act_list
# ------------------------------------------------------------------------------------------ #
def __fetch_data__(self, type):
# init html_response
html_response = ""
# open an url and read the data
try:
if type not in self.data:
if self.url[type] == "":
return False
request = urllib.request.Request(self.url[type])
else:
request = urllib.request.Request(self.url[type], self.data[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] + "! reason: " + str(e.reason))
if not self.has_already_fetched: # first fetch
sys.exit()
self.has_already_fetched = True
return html_response
# ------------------------------------------------------------------------------------------ #
def get_length(self, entry):
if entry is None or entry.source == 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
# ------------------------------------------------------------------------------------------ #
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()
#
# 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>'
"""
Aura Scheduler
Is holding the eventqueue
"""
import time
import simplejson
import datetime
import decimal
import traceback
import sqlalchemy
import logging
import threading
from modules.communication.redis.messenger import RedisMessenger
from modules.scheduling.calendar import AuraCalendarService
from libraries.database.broadcasts import Schedule, ScheduleEntry, AuraDatabaseModel
from libraries.exceptions.exception_logger import ExceptionLogger
from libraries.enum.auraenumerations import ScheduleEntryType
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 simplejson.dumps([obj._asdict()], default=alchemyencoder)
else:
return str(obj)
# ------------------------------------------------------------------------------------------ #
class AuraScheduler(ExceptionLogger, threading.Thread):
"""
Aura Scheduler Class
Gets data from pv and importer, stores and fires events
"""
redismessenger = RedisMessenger()
message_timer = []
job_result = {}
liquidsoapcommunicator = None
schedule_entries = None
active_entry = None
exit_event = None
programme = None
client = None
logger = None
config = None
tried_fetching = 0
fetch_max = 3
def __init__(self, config):
"""
Constructor
@type config: ConfigReader
@param config: read engine.ini
"""
self.config = config
self.logger = logging.getLogger("AuraEngine")
# init threading
threading.Thread.__init__(self)
# init messenger.. probably not needed anymore
self.redismessenger.set_channel('scheduler')
self.redismessenger.set_section('execjob')
# load error messages
error_file = self.config.get("install_dir") + "/errormessages/scheduler_error.js"
f = open(error_file)
self.error_data = simplejson.load(f)
f.close()
# init database ?
self.init_database()
#self.redismessenger.send('Scheduler started', '0000', 'success', 'initApp', None, 'appinternal')
# create exit event
self.exit_event = threading.Event()
# start loading new programm every hour
self.start()
# ------------------------------------------------------------------------------------------ #
def init_database(self):
# check if tables do exist. if not create them
try:
ScheduleEntry.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 run(self):
#while True:
while not self.exit_event.is_set():
# set seconds to wait
seconds_to_wait = int(self.config.get("fetching_frequency"))
# calc next time
next_time = datetime.datetime.now() + datetime.timedelta(seconds=seconds_to_wait)
# write to logger
self.logger.info("Fetch new programmes every " + str(seconds_to_wait) + "s started. Going to start next time " + str(next_time))
# empty database
# self.logger.info("emptying database")
# ScheduleEntry.truncate()
# fetch new programme
self.fetch_new_programme()
# and wait
self.exit_event.wait(seconds_to_wait)
# ------------------------------------------------------------------------------------------ #
def stop(self):
self.exit_event.set()
# ------------------------------------------------------------------------------------------ #
def get_active_entry(self):
now_unix = time.mktime(datetime.datetime.now().timetuple())
lastentry = None
# load programme if necessary
if self.programme is None:
self.logger.debug("want to get active channel, but have to load programme first")
self.load_programme_from_db()
# get active source
for entry in self.programme:
# check if lastentry is set and if act entry is in the future
if lastentry is not None and entry.entry_start_unix > now_unix:
# return entry if so
return entry # actsource = entry.source
lastentry = entry
return None
# ------------------------------------------------------------------------------------------ #
def load_programme_from_db(self, silent=False):
#self.programme = ScheduleEntry.select_all()
self.programme = ScheduleEntry.select_act_programme()
# now in unixtime
now_unix = time.mktime(datetime.datetime.now().timetuple())
# switch to check if its the first stream in loaded programme
first_stream_in_programme = False
# fading times
fade_in_time = float(self.config.get("fade_in_time"))
fade_out_time = float(self.config.get("fade_out_time"))
# old entry for fading out
old_entry = None
for entry in self.programme:
# since we get also programmes from act hour, filter these out
if entry.entry_start_unix > now_unix:
# when do we have to start?
diff = entry.entry_start_unix - now_unix
diff = diff/1000 # testing purpose
self.enable_fading(fade_in_time, fade_out_time, old_entry, entry, diff)
# create the activation threads and run them after <diff> seconds
if entry.source.startswith("linein"):
self.add_or_update_timer(diff, self.liquidsoapcommunicator.activate, [entry])
elif entry.type == ScheduleEntryType.STREAM:
if first_stream_in_programme:
self.liquidsoapcommunicator.next_stream_source(entry.source)
first_stream_in_programme = False
self.add_or_update_timer(diff, self.liquidsoapcommunicator.activate, [entry])
elif entry.type == ScheduleEntryType.FILESYSTEM:
self.add_or_update_timer(diff, self.liquidsoapcommunicator.activate, [entry])
else:
self.logger.warning("Scheduler cannot understand source '" + entry.source + "' from " + str(entry))
self.logger.warning(" Not setting any activation Thread!")
# store the old entry for fading out
old_entry = entry
self.logger.warning(self.print_message_queue())
# ------------------------------------------------------------------------------------------ #
def enable_fading(self, fade_in_time, fade_out_time, old_entry, new_entry, diff):
# enable fading when entry types are different
if old_entry is not None:
if old_entry.type != new_entry.type:
# enable fadeout if enabled
if fade_out_time != 0 and old_entry is not None:
params = [old_entry, -fade_out_time]
self.add_or_update_timer(diff, self.liquidsoapcommunicator.fade_out, params)
# same for fadein
if fade_in_time != 0:
params = [new_entry, fade_in_time]
self.add_or_update_timer(diff, self.liquidsoapcommunicator.fade_in, params)
# ------------------------------------------------------------------------------------------ #
def add_or_update_timer(self, diff, func, parameters):
length = len(parameters)
if length == 2:
entry = parameters[0]
fadingtime = parameters[1]
elif length == 1:
entry = parameters[0]
fadingtime = 0
# check if something is planned at given time
if fadingtime < 0:
planned_timer = self.is_something_planned_at_time(entry.entry_start + datetime.timedelta(seconds=fadingtime))
else:
planned_timer = self.is_something_planned_at_time(entry.entry_start)
# if something is planned on entry.entry_start
if planned_timer:
planned_entry = planned_timer.entry
# check if the playlist_id's are different
if planned_entry.playlist_id != entry.playlist_id:
# if not stop the old timer and remove it from the list
self.stop_timer(planned_timer)
# and create a new one
self.create_timer(diff, func, parameters)
# 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:
self.create_timer(diff, func, parameters)
# ------------------------------------------------------------------------------------------ #
def stop_timer(self, timer):
# stop timer
timer.cancel()
# and remove it from message queue
self.message_timer.remove(timer)
# ------------------------------------------------------------------------------------------ #
def create_timer(self, diff, func, parameters):
t = CallFunctionTimer(diff, func, parameters)
self.message_timer.append(t)
t.start()
# ------------------------------------------------------------------------------------------ #
def is_something_planned_at_time(self, given_time):
for t in self.message_timer:
if t.entry.entry_start == given_time:
return t
return False
# ------------------------------------------------------------------------------------------ #
def get_act_programme_as_string(self):
programme_as_string = ""
if self.programme is None or len(self.programme) == 0:
self.fetch_new_programme()
try:
programme_as_string = simplejson.dumps([p._asdict() for p in self.programme], default=alchemyencoder)
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):
message_queue = ""
for t in self.message_timer:
message_queue += str(t)+"\n"
return message_queue
# ------------------------------------------------------------------------------------------ #
def swap_playlist_entries(self, indexes):
from_entry = None
to_entry = None
from_idx = indexes["from_index"]
to_idx = indexes["to_index"]
# find the entries
for p in self.programme:
if p.programme_index == int(from_idx):
from_entry = p
if p.programme_index == int(to_idx):
to_entry = p
# break out of loop, if both entries found
if from_entry is not None and to_entry is not None:
break
# check if entries are found
if from_entry is None or to_entry is None:
return "From or To Entry not found!"
# swap sources
swap = from_entry.source
from_entry.source = to_entry.source
to_entry.source = swap
# store to database
from_entry.store(add=False, commit=False)
to_entry.store(add=False, commit=True)
# and return the programme with swapped entries
return self.get_act_programme_as_string()
# ------------------------------------------------------------------------------------------ #
def delete_playlist_entry(self, index):
found = False
for p in self.programme:
if p.programme_index == int(index):
p.delete(True)
self.load_programme_from_db()
found = True
break
if not found:
self.logger.warning("Nothing to delete")
return self.get_act_programme_as_string()
# ------------------------------------------------------------------------------------------ #
def insert_playlist_entry(self, fromtime_source):
fromtime = fromtime_source["fromtime"]
source = fromtime_source["source"]
entry = ScheduleEntry()
entry.entry_start = fromtime
entry.source = source
entry.playlist_id = 0
entry.schedule_id = 0
entry.entry_num = ScheduleEntry.select_next_manual_entry_num()
entry.store(add=True, commit=True)
self.load_programme_from_db()
return self.get_act_programme_as_string()
# ------------------------------------------------------------------------------------------ #
def fetch_new_programme(self):
self.logger.info("trying to fetch new programme")
if self.tried_fetching == self.fetch_max:
msg = "Cannot connect to PV or Tank! No Programme loaded!"
self.logger.error(msg)
self.tried_fetching = 0
return msg
self.tried_fetching += 1
acs = AuraCalendarService(self.config)
queue = acs.get_queue()
# start fetching thread
acs.start()
# wait for the end
response = queue.get()
if type(response) is dict:
self.load_programme_from_db()
if self.programme is not None and len(self.programme) > 0:
self.tried_fetching = 0
if len(self.programme) == 0 and self.tried_fetching == self.fetch_max:
self.logger.critical("Programme loaded from database has no entries!")
# return self.get_act_programme_as_string()
elif response.startswith("fetching_aborted"):
self.logger.warning("Fetching was being aborted from AuraCalendarService! Are you connected? Reason: " + response)
else:
self.logger.warning("Got an unknown response from AuraCalendarService: " + response)
# ------------------------------------------------------------------------------------------ #
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 = "/var/audio/fallback/eins.zwo.bombe.mp3"
elif playlistname == "timeslot":
file = "/var/audio/fallback/ratm.killing.mp3"
elif playlistname == "show":
file = "/var/audio/fallback/weezer.hash.pipe.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
# ------------------------------------------------------------------------------------------ #
class CallFunctionTimer(threading.Timer):
logger = None
param = None
entry = None
diff = None
def __init__(self, diff, func, param):
threading.Timer.__init__(self, diff, func, param)
self.diff = diff
self.func = func
self.param = param
self.entry = param[0]
self.logger = logging.getLogger("AuraEngine")
self.logger.debug(str(self))
# ------------------------------------------------------------------------------------------ #
def __str__(self):
if len(self.param) >= 2:
# fading in
if self.param[1] > 0:
return "CallFunctionTimer starting @ " + str(self.entry.entry_start) + " fading in source '" + str(self.entry.source) + "' in seconds: " + str(self.diff)
# fading out
else:
return "CallFunctionTimer starting @ " + str(self.entry.entry_start + datetime.timedelta(seconds=self.param[1])) + " fading out source '" + str(self.entry.source) + "' in seconds: " + str(self.diff+self.param[1])
else:
return "CallFunctionTimer starting @ " + str(self.entry.entry_start) + " switching to source '" + str(self.entry.source) + "' in seconds: " + str(self.diff)
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
import simplejson
import decimal
import traceback
import sqlalchemy
import datetime
import logging
from flask import request, render_template
from libraries.database.database 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 simplejson.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):
self.scheduler = scheduler
self.messenger = messenger
self.lqs_communicator = lqs_communicator
# when debug is enabled => logging messages appear twice
APP.run() #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 simplejson.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 simplejson.dumps({"Error": error})
@staticmethod
@APP.route("/api/v1/soundserver_state", methods=["GET"])
def soundserver_settings():
from modules.communication.liquidsoap.communicator import LiquidSoapCommunicator
from modules.base.config import ConfigReader
try:
cr = ConfigReader()
cr.load_config()
lqs = LiquidSoapCommunicator(cr)
return 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 simplejson.dumps({"Error": error})
@staticmethod
@APP.route("/api/v1/trackservice/", methods=["GET"])
def api_trackservice_now():
return simplejson.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 = simplejson.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
<h3>{{ _('Scheduler Jobs') }}</h3>
<table class="table table-striped scheduler-jobs">
<tr>
<th>{{ _('Job') }}</th>
<th>{{ _('Time') }}</th>
<th>{{ _('Until') }}</th>
</tr>
{% for job in jobs %}
<tr>
<td>{{ job.job }}</td>
<td>{{ job.time }}</td>
<td>{{ job.until }}</td>
</tr>
{% endfor %}
</table>
\ No newline at end of file
{% extends "layout.html" %}
{% block title %}Start{% endblock %}
{% block customcss %}<link rel="stylesheet" href="{{ url_for('static', filename='css/lib/jquery-ui.min.css') }}" type="text/css">{% endblock %}
{% block pagetitle %}{{ _('Search for broadcasts') }}{% endblock %}
{% block body %}
<form id="searchForm" classs="form-horizontal" method="POST" action="/search">
<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">
{% if event.overwrite_event %}
<a href="/search/reset/{{ event.id }}" data-hash="li-{{ event.id }}" class="pull-right btn btn-default btn-sm eventResetBtn" >
Zurücksetzen
</a>
{% else %}
<a href="/search/modal/{{ event.id }}" data-hash="li-{{ event.id }}" class="pull-right btn btn-default btn-sm eventOverwriteBtn" >
Überschreiben
</a>
{% endif %}
<h3 {% if event.overwrite_event %} style="text-decoration:line-through" {% endif %}>{{ event.title }}</h3> <!-- Button trigger modal -->
{% if event.overwrite_event %}
<h3>{{ event.overwrite_event.title }}</h3>
{% endif %}
<div>{{ event.start | formatdate }} - {{ event.end | formatdate }}</div>
{% if event.overwrite_event %}
<div>{% if event.overwrite_event.rerun %}{{ _('Repetition of') }} {{ event.overwrite_event.replay_of_datetime | formatdate }}{% endif %}</div>
{% else %}
<div>{% if event.rerun %}{{ _('Repetition of') }} {{ event.replay_of_datetime | formatdate }}{% endif %}</div>
{% endif %}
<div>{{ event.subject }}</div>
{% if event.filename %}
<div><strong>{{ _('File') }}:</strong> {{ event.filename }} <span class="glyphicon {% if event.fileExists() %}glyphicon-ok text-success{% else %}glyphicon-minus text-danger{% endif %}" aria-hidden="true"></span></div>
{% endif %}
<h4><a data-toggle="collapse" href="#{{ event.id }}"><span class="caret"> </span> {{ event.tracks|length }} Tracks </a></h4>
<div class="collapse" id="{{ event.id }}">
<uL>
{% for track in event.tracks %}
<li>{{ track.filename }} <span class="glyphicon {% if track.fileExists() %}glyphicon-ok text-success{% else %}glyphicon-minus text-danger{% endif %}" aria-hidden="true"></span></li>
{% endfor %}
</uL>
</div>
</div>
<div class="col-md-6">
<div class="well well-small">
<h4 class="text-info">{{ _('Preproduction') }}</h4>
{% if event.overrides.count() > 0 %}
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ event.procOverrides }}" aria-valuemax="100" aria-valuemin="0" style="width: {{ event.procOverrides }}%;">
{{ event.procOverrides }}%
</div>
</div>
{% endif %}
<br />
<a class="btn btn-xs btn-default" href="{{ url_for('preprod', eventid=event.id ) }}"><span class="glyphicon glyphicon-{% if event.overrides.count() > 0 %}pencil{% else %}plus{% endif %}">{% if event.overrides.count() > 0 %} {{ _('Edit') }}{% else %} {{ _('Add') }}{% endif %}</span></a>
</div>
</div>
<div class="clearfix"></div>
</li>
{% endfor %}
</uL>
<ul class=pagination>
{%- for page in pagination.iter_pages() %}
{% if page %}
{% if page == pagination.page %}
<li class="active"><a class="link active" href="#">{{ page }}</a></li>
{% else %}
<li><a class="link" data-page="{{ page }}" href="{{ url_for('search') }}">{{ page }}</a></li>
{% endif %}
{% else %}
<li><span class=ellipsis></span></li>
{% endif %}
{%- endfor %}
</ul>
<input type="hidden" id="form-page" name="page" value="{{ page }}">
</form>
<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">Suche</h4>
</div>
<div class="modal-body">
<iframe src="" style="zoom:0.60" frameborder="0" height="450" width="99.6%"></iframe>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ _('Cancel') }}</button>
</div>
</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 src="{{ url_for('static', filename='js/lib/jquery-ui.min.js') }}"></script>
<script>
function closeModal() {
$('#myModal').modal('hide')
window.location.reload(true)
}
$('.eventOverwriteBtn').click(function(e){
e.preventDefault()
var link = $(this).attr("href");
window.location.hash = $(this).attr("data-hash");
$('iframe').attr("src",link);
$('#myModal').modal({show:true})
});
$('.eventResetBtn').click(function(e){
e.preventDefault()
var link = $(this).attr("href");
var hash = $(this).attr("data-hash");
jQuery.ajax ({
url: link,
cache: false,
success: function (response) {
window.location.hash = hash;
window.location.reload(true)
},
error: function () {
console.log('Hat nicht geklappt');
}
});
});
$(function() {
$('#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>
{% endblock %}
\ No newline at end of file
<h3>Streaming</h3>
<div class="info"><span class="text-info">{{ message }}</span></div>
{% for mount in mounts %}
<div>
<h4>{{ _('Current running title') }}: {{ mount.Title }}</h4>
</div>
<table class="table table-striped channels">
<tbody>
<tr>
<th>{{ _('ListenURL') }}</th>
<td>{{ mount.ListenURL }}</td>
</tr>
<tr>
<th>{{ _('Mountpoint') }}</th>
<td>{{ mount.Name }}</td>
</tr>
<tr>
<th>{{ _('StreamStart') }}</th>
<td>{{ mount.StreamStart }}</td>
</tr>
<tr>
<th>{{ _('AudioInfo') }}</th>
<td>{{ mount.AudioInfo }}</td>
</tr>
<tr>
<th>{{ _('Genre') }}</th>
<td>{{ mount.Genre }}</td>
</tr>
<tr>
<th>{{ _('ListenerCount') }}</th>
<td>{{ mount.ListenerCount }}</td>
</tr>
<tr>
<th>{{ _('ListenerPeak') }}</th>
<td>{{ mount.ListenerPeak }}</td>
</tr>
<tr>
<th>{{ _('Public') }}</th>
<td>{{ mount.Public }}</td>
</tr>
<tr>
<th>{{ _('ServerDescription') }}</th>
<td>{{ mount.ServerDescription }}</td>
</tr>
<tr>
<th>{{ _('ServerName') }}</th>
<td>{{ mount.ServerName }}</td>
</tr>
<tr>
<th>{{ _('ServerType') }}</th>
<td>{{ mount.ServerType }}</td>
</tr>
<tr>
<th>{{ _('ServerURL') }}</th>
<td>{{ mount.ServerURL }}</td>
</tr>
<tr>
<th>{{ _('SlowListeners') }}</th>
<td>{{ mount.SlowListeners }}</td>
</tr>
<tr>
<th>{{ _('SourceIP') }}</th>
<td>{{ mount.SourceIP }}</td>
</tr>
<tr>
<th>{{ _('TotalBytesRead') }}</th>
<td>{{ mount.TotalBytesRead }}</td>
</tr>
<tr>
<th>{{ _('TotalBytesSent') }}</th>
<td>{{ mount.TotalBytesSent }}</td>
</tr>
</tbody>
</table>
{% endfor %}
<div>
<h2>System</h2>
<table class="table table-bordered">
<tr>
<th>{{ _('Load') }}</th>
<th>{{ _('Free Space') }}</th>
</tr>
<tr>
<td>{{ load }}</td>
<td>{{ diskfree }}</td>
</tr>
</table>
</div>
<div class="row-fluid">
<h2>{{ _('Modules') }}</h2>
{% for state in componentStates %}
<div class="col-sm-4 col-md-3 col-lg-2">
<div id="controller-alive" class="alive {% if state.state %}bg-success{% else %}bg-danger{% endif %}">
<div class="center-block">
<h4>{{ state.title }}</h4>
<p>{% if state.state %}
{{ state.title }} is alive
{% else %}
{{ state.title }} is down
{% endif %}
</p>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="clearfix"></div>
<div>
<table class="table table-striped">
<tr>
<th>{{ _('Playlist Status') }}</th>
<td>
{% if playerState %}
<div><strong>{{ _('File') }}:</strong> {{ playerState.file }}</div>
{% if not playerState.complete %}
<div class="text-warning">
<strong>{{ _('Warning: recording was interrupted at') }} {{ playerState.recorded }}%!!!</strong>
</div>
{% endif %}
{% else %}
{{ _('Playlist not playing') }}
{% endif %}
</td>
<td>
{% if playerState %}
{{ _('Next event') }}: <i>play audio <strong>{{ trackStart.location }}</strong> at {{ trackStart.starts }}</i>
{% else %}
{{ _('Next Start') }} {{ playlistStart }}
{% endif %}
</td>
</tr>
<tr>
<th width="20%">{{ _('Recorder Status') }}</th>
<td>{% if recorderState %}
<div class="center-block text-center">{{ recorderState.file }}</div>
<div class="clearfix"></div>
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-striped active" role="progressbar" aria-valuenow="{{ recorderState.recorded }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ recorderState.recorded }}%;">
{{ recorderState.recorded }}%
</div>
</div>
{% else %}
{{ _('Not recording') }}
{% endif %}</td>
<td>
{{ _('Next event') }}: <i>record <strong>{{ recordStart.location }}</strong> at {{ recordStart.starts }}</i>
</td>
</tr>
</table>
</div>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
{{ length }} trackservice entries on {{ selected_date }} found
{% for entry in trackservice_entries %}
{{ entry.start }}
{% endfor %}
</body>
</html>
\ No newline at end of file
Flask==0.12.2
Flask-SQLAlchemy==2.3.2
mysqlclient==1.3.12
redis==2.10.6
simplejson==3.13.2
mutagen==1.40
validators==0.12.1
[Unit]
Description=Aura Engine Playout Server
After=network.target
[Service]
Type=simple
User=gg
WorkingDirectory=/home/gg/PycharmProjects/engine
ExecStart=/home/gg/PycharmProjects/engine/aura.py
ExecStop=/home/gg/PycharmProjects/engine/guru.py --shutdown --quiet
Restart=always
[Install]
WantedBy=multi-user.target
[Unit]
Description=Aura Engine Playout Server
After=network.target aura-engine.service
Wants=aura-engine.service
[Service]
Type=simple
User=gg
ExecStart=/usr/bin/liquidsoap /home/gg/PycharmProjects/engine/modules/liquidsoap/engine.liq
Restart=always
[Install]
WantedBy=multi-user.target
#!/usr/bin/python3
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
from libraries.database.broadcasts import *
import simplejson
import logging
import sqlalchemy
import decimal
from modules.communication.liquidsoap.communicator import LiquidSoapCommunicator
from modules.monitoring.diskspace_watcher import DiskSpaceWatcher
from libraries.base.config import AuraConfig
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 simplejson.dumps([obj._asdict()], default=alchemyencoder)
else:
return str(obj)
# programme_as_string = simplejson.dumps([se[0]._asdict()], default=alchemyencoder)
# print(programme_as_string)
def start_diskspace_watcher():
config = AuraConfig()
config.read_config()
diskspace_watcher = DiskSpaceWatcher(config.config, logging.getLogger("AuraEngine"), LiquidSoapCommunicator(config.config))
diskspace_watcher.run()
def select_act_programme():
programme = ScheduleEntry.select_act_programme()
for p in programme:
print(p)
def fadeout(lsc):
entry = ScheduleEntry.select_act_programme()
lsc.fade_out(entry, 2)
def fadein(lsc):
entry = ScheduleEntry.select_act_programme()
lsc.fade_in(entry, 1)
# # ## ## ## ## ## # #
# # ENTRY FUNCTION # #
# # ## ## ## ## ## # #
def main():
#start_diskspace_watcher()
#select_act_programme()
config = AuraConfig()
config.read_config()
lsc = LiquidSoapCommunicator(config.config)
fadeout(lsc)
fadein(lsc)
# # ## ## ## ## ## ## # #
# # End ENTRY FUNCTION # #
# # ## ## ## ## ## ## # #
if __name__ == "__main__":
main()
import os
import unittest
import validators
from datetime import datetime
# libraries.base
from libraries.base.logger import AuraLogger
from libraries.base.config import AuraConfig
# libraries.database
from libraries.database.broadcasts import Schedule, ScheduleEntry, TrackService
# libraries.security
# from libraries.security.user import AuraUser
# modules
from modules.communication.liquidsoap.communicator import LiquidSoapCommunicator
from modules.scheduling.scheduler import AuraScheduler
class TestLogger(unittest.TestCase):
aura_logger = None
def setUp(self):
self.aura_logger = AuraLogger()
def test_logger(self):
self.assertTrue(self.aura_logger.logger.hasHandlers())
class TestConfig(unittest.TestCase):
aura_config = None
def setUp(self):
self.aura_config = AuraConfig()
def test_config(self):
# is ini path correct set?
self.assertEqual(self.aura_config.config.ini_path, "/etc/aura/engine.ini")
# install_dir is set by runtime. is it a directory?
self.assertTrue(os.path.isdir(self.aura_config.config.get("install_dir")))
# calendarurl and importerurl set and valid urls?
self.assertTrue(validators.url(self.aura_config.config.get("calendarurl")))
self.assertTrue(validators.url(self.aura_config.config.get("importerurl")))
# is liquidsoap socketdir set and a directory?
self.assertTrue(os.path.isdir(self.aura_config.config.get("socketdir")))
# database settings set?
self.assertIsNotNone(self.aura_config.config.get("db_user"))
self.assertIsNotNone(self.aura_config.config.get("db_pass"))
self.assertIsNotNone(self.aura_config.config.get("db_name"))
self.assertIsNotNone(self.aura_config.config.get("db_host"))
class TestSchedule(unittest.TestCase):
schedule = None
def setUp(self):
self.schedule = Schedule()
def test_schedule(self):
# select one and check if its not None and a Schedule
entry = self.schedule.select_by_id(1)
self.assertIsNotNone(entry)
self.assertIsInstance(entry, Schedule)
class TestScheduleEntry(unittest.TestCase):
schedule_entry = None
def setUp(self):
self.schedule_entry = ScheduleEntry()
def test_schedule_entry(self):
# select one playlist and check if its not None, a ScheduleEntry
entry = self.schedule_entry.select_playlist(2)
self.assertIsNotNone(entry)
self.assertIsInstance(entry, list)
self.assertGreaterEqual(len(entry), 1)
class TestTrackService(unittest.TestCase):
track_service = None
def setUp(self):
self.track_service = TrackService()
def test_track_service(self):
day = datetime.strptime("19.03.2018", "%d.%m.%Y")
entry = self.track_service.select_by_day(day)
self.assertIsNotNone(entry)
self.assertIsInstance(entry, list)
class TestAuraUser(unittest.TestCase):
aura_user = None
def setUp(self):
self.aura_user = AuraUser()
def test_add_user(self):
username = "user"
password = "password"
role = "admin"
login_cnt = len(self.aura_user.getLogins())
# insert user
key = self.aura_user.insertUser(username, password, role)
self.assertGreaterEqual(len(self.aura_user.getLogins()), login_cnt)
# selecting user and check data
user = self.aura_user.getUserByKey(key)
self.assertEqual(user["username"], username)
# TODO: no encrypted storage.., but usermgm not really in use
self.assertEqual(user["password"], password)
self.assertEqual(user["role"], role)
class TestLQSComm(unittest.TestCase):
comm = None
def setUp(self):
# wosn do passiert?
p = AuraConfig().config
self.comm = LiquidSoapCommunicator(p)
self.comm.scheduler = AuraScheduler(p)
self.comm.init_player()
def test_get_active_channel(self):
active_channel = self.comm.get_active_channel()
print(active_channel)
if __name__ == '__main__':
unittest.main()
\ No newline at end of file