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 2603 additions and 0 deletions
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
import 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 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
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()
self.__set_url__("calendar")
self.__set_url__("importer")
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 fetched_schedule_data is None:
self.queue.put("fetching_aborted Nothing fetched")
return
ret_schedule = []
# 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
for schedule in fetched_schedule_data:
# store the schedule
schedule_db = self.store_schedule(schedule)
# store playlists to play
self.logger.warning("only storing playlist")
self.store_playlist(schedule_db, schedule_db.playlist_id, 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)
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):
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:
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):
playlist_db = Playlist.select_playlist_for_schedule(schedule_db.schedule_start, playlist_id)
havetoadd = False
if not playlist_db:
playlist_db = Playlist()
havetoadd = True
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
playlist_db.entry_count = len(fetched_playlist["entries"])
playlist_db.store(havetoadd, True)
self.store_playlist_entries(playlist_db, fetched_playlist)
return playlist_db
def store_playlist_entries(self, playlist_db, fetched_playlist):
entry_num = 0
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
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.duration = entry["file"]["duration"]
playlistentry_db.store(havetoadd, True)
self.store_playlist_entry_metadata(playlistentry_db, entry["file"]["metadata"])
entry_num = entry_num + 1
def store_playlist_entry_metadata(self, playlistentry_db, metadata):
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 = "N/a"
else:
playlistentrymetadata_db.artist = metadata["artist"]
playlistentrymetadata_db.title = metadata["title"]
if "album" in metadata:
playlistentrymetadata_db.album = metadata["album"]
playlistentrymetadata_db.store(havetoadd, True)
# ------------------------------------------------------------------------------------------ #
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
# ------------------------------------------------------------------------------------------ #
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
class CalendarFetcher:
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):
self.config = config
self.logger = logging.getLogger("AuraEngine")
self.__set_url__("calendar")
self.__set_url__("importer")
self.__set_url__("api_show_")
def fetch(self):
# 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["calendar"] + "! 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["calendar"] + "! 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["importer"] + "! 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["importer"] + "! 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
# ------------------------------------------------------------------------------------------ #
def __set_url__(self, type):
url = self.config.get(type+"url")
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):
servicetype = "calendar"
schedule = None
# fetch data from steering
html_response = self.__fetch_data__(servicetype)
# FIXME move hardcoded test-data to separate testing logic.
# use testdata if response fails or is empty
if not html_response or html_response == b"[]":
self.logger.critical("Got no response from Steering!")
#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"}]'
# use testdata if wanted
if self.config.get("use_test_data"):
# FIXME move hardcoded test-data to separate testing logic.
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!")
else:
html_response = "{}"
# convert to dict
schedule = simplejson.loads(html_response)
# check data
self.logger.critical("no JSON data checks. I believe what i get here")
#self.fetched_schedule_data = self.remove_unnecessary_data(schedule)
return self.remove_unnecessary_data(schedule)
# ------------------------------------------------------------------------------------------ #
def __fetch_schedule_playlists__(self):
# store fetched entries => do not have to fetch playlist_id more than once
fetched_entries=[]
try:
self.logger.warning("only fetching normal playlists. no fallbacks")
for schedule in self.fetched_schedule_data:
# Enhance schedule with details of show (e.g. slug)
schedule = self.__fetch_show_details__(schedule)
# retrieve playlist and the fallbacks 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)
self.logger.info(str(schedule))
except Exception as e:
self.logger.error("Error: "+str(e))
# ------------------------------------------------------------------------------------------ #
def __fetch_show_details__(self, schedule):
servicetype = "api_show_"
json_response = self.__fetch_data__(servicetype, "${ID}", str(schedule["show_id"]))
show_details = simplejson.loads(json_response)
# Augment "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_schedule_entries):
servicetype = "importer"
# fetch playlists from TANK
if not "show_slug" in schedule:
raise ValueError("Missing 'show_slug' for schedule", schedule)
slug = str(schedule["show_slug"])
json_response = self.__fetch_data__(servicetype, "${SLUG}", slug)
# if a playlist is already fetched, do not fetch it again
for entry in fetched_schedule_entries:
# FIXME schedule["playlist_id"] is always None, review if entry["id"] is valid
if entry["id"] == schedule[id_name]:
self.logger.debug("playlist #" + str(schedule[id_name]) + " already fetched")
return entry
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
schedule_entries = simplejson.loads(json_response)
if "results" in schedule_entries:
schedule_entries = schedule_entries["results"][0]
for entry in schedule_entries["entries"]:
if entry["uri"].startswith("file"):
entry["filename"] = self.convert_to_filename(entry["uri"])
fetched_schedule_entries.append(schedule_entries)
return schedule_entries
def convert_to_filename(self, uri):
# convert to normal filename
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, placeholder=None, value=None):
# Init html_response
html_response = b''
url = self.__build_url__(type, placeholder, value)
# 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):
url = self.url[type]
if placeholder:
url = url.replace(placeholder, value)
# print("built URL: "+url)
return url
# ------------------------------------------------------------------------------------------ #
def remove_unnecessary_data(self, schedule):
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, 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 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) 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.communication.redis.messenger import RedisMessenger
from modules.scheduling.calendar import AuraCalendarService
from libraries.database.broadcasts import Schedule, Playlist, AuraDatabaseModel
from libraries.exceptions.exception_logger import ExceptionLogger
from libraries.enum.auraenumerations import ScheduleEntryType, TimerType
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
- Gets data from Steering and Tanks
- 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 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 = []
#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.redismessenger = RedisMessenger(config)
# init threading
threading.Thread.__init__(self)
# init messenger.. 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()
# Start thread to load new programme info every hour
self.start()
def run(self):
"""
Called when thread is started via `start()`. It calls `self.fetch_new_programme()`
periodically depending on the `fetching_frequency` define engine configuration.
"""
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)))
# empty database
# self.logger.info("emptying database")
# ScheduleEntry.truncate()
self.fetch_new_programme()
self.exit_event.wait(seconds_to_wait)
#
# PUBLIC METHODS
#
def get_active_entry(self):
"""
Retrieves the current `Show` and `Track` tuple being played.
Externally called via `LiquidSoapCommunicator`.
Returns:
(Show, Entry): The show and track to be played next.
"""
# now_unix = time.mktime(datetime.datetime.now().timetuple())
# lastentry = None
# # Load programme if necessary
# if self.programme is None:
# self.logger.info("Next track requested: Need to load programme from database first.")
# self.load_programme_from_db()
# # Get the entry currently being played
# for show in self.programme:
# for entry in show.playlist:
# # check if lastentry is set and if act entry is in the future
# if lastentry is not None and entry.start_unix > now_unix:
# # return entry if so
# return (show,entry) # actsource = entry.source
# lastentry = entry
# return None, None
# FIXME active_entry logic
if not self.active_entry:
self.logger.warning("No active entry set! Is currently nothing or a fallback playing?")
return (None, None)
else:
return self.active_entry
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. tracks 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: " + 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 file to be played for a given fallback-type.
Valid fallback-types are:
* timeslot
* show
* station
Returns:
(String): Absolute path to the file to be played as a fallback.
"""
self.logger.critical("HAVE TO <GET> NEXT FILE FOR: " + fallbackname)
(show, entry) = self.get_active_entry()
self.logger.critical(str(show) + " " + str(entry))
if fallbackname == "timeslot":
file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
elif fallbackname == "show":
file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
elif fallbackname == "station":
file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
else:
file = ""
self.logger.critical("Should set next fallback file for " + fallbackname + ", but this playlist is unknown!")
#set_next_file_thread = SetNextFile(fallbackname, show)
#set_next_file_thread.start()
self.logger.info("Got next fallback file for '" + fallbackname + "': " + file)
# self.redismessenger.set_next_file_for(playlistname, file)
return file
#
# PRIVATE METHODS
#
def fetch_new_programme(self):
"""
Fetch the latest programme from `AuraCalendarService`.
In case no programme is successfully returned, it is tried
to retrieve the programme from Engine's database.
"""
self.logger.info("Trying to fetch new program...")
acs = AuraCalendarService(self.config)
queue = acs.get_queue()
acs.start() # start fetching thread
response = queue.get() # wait for the end
# 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()
if len(self.programme) == 0:
self.logger.critical("Programme fetched from Steering/Tank has no entries!")
# return self.get_act_programme_as_string()
elif response.startswith("fetching_aborted"):
# TODO Check why the 16th entry is logged only
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)
# if somehow the programme could not be fetched => try to load it from database
#if self.last_successful_fetch is None:
self.last_successful_fetch = lsf
self.load_programme_from_db()
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 self.programme is None or len(self.programme) == 0:
self.logger.critical("Could not load programme from database. We are in big trouble my friend!")
return
planned_entries = []
for schedule in self.programme:
# playlist to play
schedule.playlist = Playlist.select_playlist(schedule.playlist_id)
# show fallback is played when playlist fails
schedule.showfallback = Playlist.select_playlist(schedule.show_fallback_id)
# timeslot fallback is played when show fallback fails
schedule.timeslotfallback = Playlist.select_playlist(schedule.timeslot_fallback_id)
# station fallback is played when timeslot fallback fails
schedule.stationfallback = Playlist.select_playlist(schedule.station_fallback_id)
for p in schedule.playlist:
planned_entries.append(p)
# FIXME Same playlists are repeated over time - test with different schedules/timeslots/playlists
# Therefore only passing the first playlist for now:
self.logger.warn("ONLY PASSING 1ST PLAYLIST OF PROGRAMME")
self.enable_entries(planned_entries[0])
self.print_message_queue()
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:
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 the activation threads and run them after <diff> seconds
self.logger.critical("ENABLING SWITCHTIMER FOR " + str(entry))
entry.switchtimer = self.add_or_update_timer(diff, self.liquidsoapcommunicator.activate, [entry])
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
# FIXME check we there's an array passed
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):
threading.Timer.__init__(self, diff, func, param)
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
self.logger = logging.getLogger("AuraEngine")
self.logger.debug(str(self))
# ------------------------------------------------------------------------------------------ #
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
<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
sqlalchemy==1.3.11
Flask==0.12.2
Flask-SQLAlchemy==2.3.2
mysqlclient==1.3.12
redis==2.10.6
mutagen==1.40
validators==0.12.1
#/usr/bin/bash
mode="engine"
#debug="--debug --verbose"
debug=""
if [ -n "$1" ]; then
if [[ $1 =~ ^(engine|lqs)$ ]]; then
mode=$1
fi
fi
echo "[ Run mode=$mode ]"
if [ $mode == "engine" ]; then
/usr/bin/python3.6 aura.py
fi
if [ $mode == "lqs" ]; then
(cd modules/liquidsoap/ && /usr/bin/liquidsoap $debug ./engine.liq)
fi
\ No newline at end of file
[Unit]
Description=Aura Engine - Playout Server
After=network.target
[Service]
Type=simple
User=david
WorkingDirectory=/home/david/Code/aura/engine
ExecStart=/home/david/Code/aura/engine/aura.py
ExecStop=/home/david/Code/aura/engine/guru.py --shutdown --quiet
Restart=always
[Install]
WantedBy=multi-user.target
[Unit]
Description=Aura Engine - Liquidsoap Server
After=network.target aura-engine.service
Wants=aura-engine.service
[Service]
Type=simple
User=david
ExecStart=/usr/bin/liquidsoap /home/david/Code/aura/engine/modules/liquidsoap/engine.liq
Restart=always
[Install]
WantedBy=multi-user.target
#!/usr/bin/python3.6
#
# 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 json
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
from libraries.database.broadcasts import Schedule, ScheduleEntry
from modules.scheduling.scheduler import AuraScheduler, AuraCalendarService
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)
# programme_as_string = json.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():
# start_diskspace_watcher()
# select_act_programme()
config = AuraConfig()
config.read_config()
liquidsoapcommunicator = LiquidSoapCommunicator(config.config)
sched = AuraScheduler(config.config)
liquidsoapcommunicator.scheduler = sched
sched.liquidsoapcommunicator = liquidsoapcommunicator
programme = sched.load_programme_from_db()
for show in programme:
print(show)
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)
def fetch_new_programme():
config = AuraConfig()
config.read_config()
acs = AuraCalendarService(config.config)
queue = acs.get_queue()
# start fetching thread
acs.start()
# wait for the end
response = queue.get()
# # ## ## ## ## ## # #
# # ENTRY FUNCTION # #
# # ## ## ## ## ## # #
def main():
fetch_new_programme()
# # ## ## ## ## ## ## # #
# # End ENTRY FUNCTION # #
# # ## ## ## ## ## ## # #
if __name__ == "__main__":
main()
File: ernie_mayne_sugar.mp3
Title: Sugar, Performed by Ernie Mayne
License: Public Domain
Source: http://www.digitalhistory.uh.edu/music/music.cfm
\ No newline at end of file