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 1953 additions and 0 deletions
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import threading
import time
import json
import decimal
import traceback
import sqlalchemy
from operator import attrgetter
from datetime import datetime, timedelta
from modules.cli.redis.messenger import RedisMessenger
from modules.base.utils import SimpleUtil, EngineUtil
from modules.base.models import AuraDatabaseModel, Schedule, Playlist
from modules.base.exceptions import NoActiveScheduleException, LoadSourceException
from modules.core.channels import ChannelType, TransitionType, EntryPlayState
from modules.scheduling.types import EntryQueueState
from modules.scheduling.calendar import AuraCalendarService
from modules.scheduling.fallback_manager import FallbackManager
# FIXME this is probably not needed?
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(threading.Thread):
"""
Aura Scheduler Class
- Retrieves data from Steering and Tank
- Stores and fires events for the sound-system
Attributes:
config (AuraConfig): Holds the Engine Configuration
logger: The logger
exit_event(threading.Event): Used to exit the thread if requested
soundsystem: Virtual mixer
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(List<threading.Timer>): The timer queue of sound-system commands for playlists/entries to be played
"""
redismessenger = None
job_result = {}
config = None
logger = None
exit_event = None
soundsystem = None
last_successful_fetch = None
programme = None
message_timer = []
fallback_manager = None
client = None
is_soundsytem_ready = None
is_initialized = None
def __init__(self, config, soundsystem, func_on_init):
"""
Constructor
Args:
config (AuraConfig): Reads the engine configuration
soundsystem (SoundSystem): The soundsystem to play the schedule on
func_on_init (Function): The function to be called when the scheduler is initialized
"""
self.config = config
self.logger = logging.getLogger("AuraEngine")
self.init_database()
self.fallback_manager = FallbackManager(config, self.logger, self)
self.redismessenger = RedisMessenger(config)
self.soundsystem = soundsystem
self.soundsystem.scheduler = self
self.is_soundsytem_init = False
# Scheduler Initialization
self.is_initialized = False
self.func_on_initialized = func_on_init
# init threading
threading.Thread.__init__(self)
# init messenger.. FIXME probably not needed anymore
self.redismessenger.set_channel('scheduler')
self.redismessenger.set_section('execjob')
#self.redismessenger.send('Scheduler started', '0000', 'success', 'initApp', None, 'appinternal')
# Create exit event
self.exit_event = threading.Event()
self.start()
def run(self):
"""
Called when thread is started via `start()`. It does the following:
1. `self.fetch_new_programme()` periodically from the API depending on the `fetching_frequency` defined in the engine configuration.
2. Loads the latest programme from the database and sets the instance state `self.programme` with current schedules.
3. Queues all schedules of the programme, if the soundssystem is ready to accept commands.
4. As long the scheduling window is not reached any existing, queued item will be re-evaluationed if it has changed or if some
playlist is valid. If not the relevant fallback playlist will be queued (compare "pro-active fallback handling").
On every cycle the configuration file is reloaded, to allow modifications while running the engine.
"""
while not self.exit_event.is_set():
try:
self.config.load_config()
seconds_to_wait = int(self.config.get("fetching_frequency"))
self.logger.info(SimpleUtil.cyan("== start fetching new schedules =="))
next_time = str(datetime.now())
self.logger.info("Fetching new schedules every %ss. Next fetching at %ss." % (str(seconds_to_wait), next_time))
self.fetch_new_programme()
# The scheduler is ready
if not self.is_initialized:
self.is_initialized = True
if self.func_on_initialized:
self.func_on_initialized()
# The soundsystem is ready
if self.is_soundsytem_init:
self.queue_programme()
except Exception as e:
self.logger.critical(SimpleUtil.red("Unhandled error while fetching & scheduling new programme! (%s)" % str(e)), e)
self.clean_timer_queue()
self.print_timer_queue()
self.exit_event.wait(seconds_to_wait)
#
# PUBLIC METHODS
#
def on_initialized(self):
"""
Called when the sound-sytem is connected and ready to play.
"""
self.is_soundsytem_init = True
def on_ready(self):
"""
Called when the soundsystem is ready.
"""
# self.queue_programme()
self.logger.info(self.get_ascii_programme())
try:
self.play_active_entry()
self.queue_startup_entries()
except NoActiveScheduleException:
# That's not good, but keep on working...
pass
def play_active_entry(self):
"""
Plays the entry scheduled for the very current moment and forwards to the scheduled position in time.
Usually called when the Engine boots.
Raises:
(NoActiveScheduleException): If there's no schedule in the programme, within the scheduling window
"""
sleep_offset = 10
active_schedule = self.get_active_schedule()
active_entry = self.get_active_entry()
if not active_entry:
raise NoActiveScheduleException
# In case of a file-system source, we need to fast-foward to the current marker as per schedule
if active_entry.get_type() == ChannelType.FILESYSTEM:
# Calculate the seconds we have to fast-forward
now_unix = self.get_virtual_now()
seconds_to_seek = now_unix - active_entry.start_unix
# If the seek exceeds the length of the current track,
# there's no need to do anything - the scheduler takes care of the rest
if (seconds_to_seek + sleep_offset) > active_entry.duration:
self.logger.info("The FFWD [>>] range exceeds the length of the entry. Drink some tea and wait for the sound of the next entry.")
else:
# Pre-roll and play active entry
self.soundsystem.preroll(active_entry)
self.soundsystem.play(active_entry, TransitionType.FADE)
# Check if this is the last item of the schedule
# if active_entry.end_unix > active_entry.playlist.schedule.end_unix:
# self.queue_end_of_schedule(active_schedule, True)
# Fast-forward to the scheduled position
if seconds_to_seek > 0:
# Without plenty of timeout (10s) the seek doesn't work
seconds_to_seek += sleep_offset
time.sleep(sleep_offset)
self.logger.info("Going to fast-forward %s seconds" % seconds_to_seek)
self.soundsystem.enable_transaction()
response = self.soundsystem.playlist_seek(active_entry.channel, seconds_to_seek)
self.soundsystem.disable_transaction()
self.logger.info("Sound-system seek response: " + response)
elif active_entry.get_type() == ChannelType.HTTP \
or active_entry.get_type() == ChannelType.HTTPS \
or active_entry.get_type() == ChannelType.LIVE:
# Pre-roll and play active entry
self.soundsystem.preroll(active_entry)
self.soundsystem.play(active_entry, TransitionType.FADE)
# self.queue_end_of_schedule(active_schedule, True)
else:
self.logger.critical("Unknown Entry Type: %s" % active_entry)
# Queue the fade-out of the schedule
if active_schedule and not active_schedule.fadeouttimer:
self.queue_end_of_schedule(active_schedule, True)
def get_active_entry(self):
"""
Retrieves the current `PlaylistEntry` which should be played as per programme.
Returns:
(PlaylistEntry): The track which is (or should) currently being played
"""
now_unix = self.get_virtual_now()
# Load programme if necessary
if not self.programme:
self.load_programme_from_db()
# Check for current schedule
current_schedule = self.get_active_schedule()
if not current_schedule:
self.logger.warning(SimpleUtil.red("There's no active schedule"))
return None
# Check for scheduled playlist
current_playlist = self.fallback_manager.resolve_playlist(current_schedule)
if not current_playlist:
msg = "There's no active playlist for a current schedule. Most likely the playlist was never available or finished before the end of the schedule."
self.logger.warning(SimpleUtil.red(msg))
return None
# Iterate over playlist entries and store the current one
current_entry = None
for entry in current_playlist.entries:
if entry.start_unix <= now_unix and now_unix <= entry.end_unix:
current_entry = entry
break
if not current_entry:
# Nothing playing ... fallback will kick-in
msg = "There's no entry scheduled for playlist '%s' at %s" % (str(current_playlist), SimpleUtil.fmt_time(now_unix))
self.logger.warning(SimpleUtil.red(msg))
return None
return current_entry
def get_active_schedule(self):
"""
Retrieves the schedule currently to be played.
Returns:
(Schedule): The current schedule
"""
current_schedule = None
now_unix = self.get_virtual_now()
# Iterate over all schedules and find the one to be played right now
if self.programme:
for schedule in self.programme:
if schedule.start_unix <= now_unix and now_unix < schedule.end_unix:
current_schedule = schedule
break
return current_schedule
def get_next_schedules(self, max_count=0):
"""
Retrieves the schedules to be played after the current one.
Args:
max_count (Integer): Maximum of schedules to return, if `0` all exitsing ones are returned
Returns:
([Schedule]): The next schedules
"""
now_unix = self.get_virtual_now()
next_schedules = []
for schedule in self.programme:
if schedule.start_unix > now_unix:
if (len(next_schedules) < max_count) or max_count == 0:
next_schedules.append(schedule)
else:
break
return next_schedules
# FIXME Review relevance.
def get_act_programme_as_string(self):
"""
Fetches the latest programme and returns it as `String`.
Also used by `ServerRedisAdapter`.
Return:
(String): Programme
Raises:
(Exception): In case the programme cannot be converted to String
"""
programme_as_string = ""
if self.programme is None or len(self.programme) == 0:
self.fetch_new_programme()
try:
programme_as_string = json.dumps([p._asdict() for p in self.programme], default=alchemyencoder)
# FIXME Change to more specific exception
except Exception as e:
self.logger.error("Cannot transform programme into JSON String. Reason: " + str(e))
traceback.print_exc()
return programme_as_string
def print_timer_queue(self):
"""
Prints the current timer queue i.e. playlists in the queue to be played.
"""
message_queue = ""
messages = sorted(self.message_timer, key=attrgetter('diff'))
if not messages:
self.logger.warning(SimpleUtil.red("There's nothing in the Timer Queue!"))
else:
for msg in messages:
message_queue += str(msg)+"\n"
self.logger.info("Timer queue: \n" + message_queue)
def clean_timer_queue(self):
"""
Removes inactive timers from the queue.
"""
len_before = len(self.message_timer)
self.message_timer[:] = [m for m in self.message_timer if m.is_alive()]
len_after = len(self.message_timer)
self.logger.debug("Removed %s finished timer objects from queue" % (len_before - len_after))
# ------------------------------------------------------------------------------------------ #
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(0)))
if playlistname == "station":
file = "/home/david/Code/aura/engine2/test/content/ernie_mayne_sugar.mp3"
elif playlistname == "timeslot":
file = "/home/david/Code/aura/engine2/test/content/ernie_mayne_sugar.mp3"
elif playlistname == "show":
file = "/home/david/Code/aura/engine2/test/content/ernie_mayne_sugar.mp3"
else:
file = ""
self.logger.critical("Should set next fallback file for " + playlistname + ", but this playlist is unknown!")
self.logger.info("Set next fallback file for " + playlistname + ": " + file)
self.redismessenger.set_next_file_for(playlistname, file)
return file
def get_next_file_for(self, fallbackname):
"""
Evaluates the next **fallback files/playlists** to be played for a given fallback-type.
Valid fallback-types are:
* timeslot
* show
* station
Args:
fallbackname (String): The name of the fallback-type
Returns:
(String): Absolute path to the file to be played as a fallback.
"""
file = self.fallback_manager.get_fallback_for(fallbackname)
if file:
self.logger.info("Got next file '%s' (fallback type: %s)" % (file, fallbackname))
#set_next_file_thread = SetNextFile(fallbackname, show)
#set_next_file_thread.start()
#self.redismessenger.set_next_file_for(playlistname, file)
return file
def get_ascii_programme(self):
"""
Creates a printable version of the current programme (playlists and entries as per schedule)
Returns:
(String): An ASCII representation of the programme
"""
active_schedule = self.get_active_schedule()
s = "\n\n PLAYING NOW:"
s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
if active_schedule:
planned_playlist = None
if active_schedule.playlist:
planned_playlist = active_schedule.playlist[0] # FIXME Improve model without list
resolved_playlist = self.fallback_manager.resolve_playlist(active_schedule)
type = str(EngineUtil.get_playlist_type(resolved_playlist.fallback_type))
s += "\n│ Playing schedule %s " % active_schedule
if planned_playlist:
if resolved_playlist and resolved_playlist.playlist_id != planned_playlist.playlist_id:
s += "\n│ └── Playlist %s " % planned_playlist
s += "\n"
s += SimpleUtil.red("↑↑↑ That's the originally planned playlist.") + ("Instead playing the `%s` playlist below ↓↓↓" % SimpleUtil.cyan(type))
if resolved_playlist:
if not planned_playlist:
s += "\n"
s += SimpleUtil.red("No Playlist assigned to schedule. Instead playing the `%s` playlist below ↓↓↓" % SimpleUtil.cyan(type))
s += "\n│ └── Playlist %s " % resolved_playlist
active_entry = self.get_active_entry()
# Finished entries
for entry in resolved_playlist.entries:
if active_entry == entry:
break
else:
s += self.build_entry_string("\n│ └── ", entry, True)
# Entry currently being played
if active_entry:
s += "\n│ └── Entry %s | %s " % \
(str(entry.entry_num+1), SimpleUtil.green("PLAYING > "+str(active_entry)))
# Open entries for current playlist
rest_of_playlist = active_entry.get_next_entries(False)
entries = self.preprocess_entries(rest_of_playlist, False)
s += self.build_playlist_string(entries)
else:
s += "\n│ └── %s" % (SimpleUtil.red("No active playlist. There should be at least some fallback playlist running..."))
else:
s += "\n│ Nothing. "
s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────"
s += "\n PLAYING NEXT:"
s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
next_schedules = self.get_next_schedules()
if not next_schedules:
s += "\n│ Nothing. "
else:
for schedule in next_schedules:
resolved_playlist = self.fallback_manager.resolve_playlist(schedule)
type = str(EngineUtil.get_playlist_type(resolved_playlist.fallback_type))
s += "\n│ Queued schedule %s " % schedule
s += "\n│ └── Playlist %s (Type: %s)" % (resolved_playlist, SimpleUtil.cyan(type))
if resolved_playlist.end_unix > schedule.end_unix:
s += "\n│ %s! " % \
(SimpleUtil.red("↑↑↑ Playlist #%s ends after Schedule #%s!" % (resolved_playlist.playlist_id, schedule.schedule_id)))
entries = self.preprocess_entries(resolved_playlist.entries, False)
s += self.build_playlist_string(entries)
s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────\n\n"
return s
def build_playlist_string(self, entries):
"""
Returns a stringified list of entries
"""
s = ""
is_out_of_schedule = False
for entry in entries:
if entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE and not is_out_of_schedule:
s += "\n│ %s" % \
SimpleUtil.red("↓↓↓ These entries won't be played because they are out of schedule.")
is_out_of_schedule = True
s += self.build_entry_string("\n│ └── ", entry, is_out_of_schedule)
return s
def build_entry_string(self, prefix, entry, strike):
"""
Returns an stringified entry.
"""
s = ""
if entry.queue_state == EntryQueueState.CUT:
s = "\n│ %s" % SimpleUtil.red("↓↓↓ This entry is going to be cut.")
if strike:
entry_str = SimpleUtil.strike(entry)
else:
entry_str = str(entry)
entry_line = "%sEntry %s | %s" % (prefix, str(entry.entry_num+1), entry_str)
return s + entry_line
#
# PRIVATE METHODS
#
def get_virtual_now(self):
"""
Liquidsoap is slow in executing commands, therefore it's needed to schedule
actions by (n) seconds in advance, as defined in the configuration file by
the property `lqs_delay_offset`.
Returns:
(Integer): the Unix epoch timestamp including the offset
"""
time_offset = int(self.config.lqs_delay_offset)
return SimpleUtil.timestamp() + time_offset
def filter_scheduling_window(self, schedules):
"""
Ignore schedules which are beyond the scheduling window. The end of the scheduling window
is defined by the config option `scheduling_window_end`. This value defines the seconds
minus the actual start time of the schedule.
"""
now_unix = self.get_virtual_now()
len_before = len(schedules)
window_start = self.config.get("scheduling_window_start")
window_end = self.config.get("scheduling_window_end")
schedules = list(filter(lambda s: (s.start_unix - window_end) > now_unix and (s.start_unix - window_start) < now_unix, schedules))
len_after = len(schedules)
self.logger.info("For now, skipped %s future schedule(s) which are out of the scheduling window (-%ss <-> -%ss)" % ((len_before - len_after), window_start, window_end))
return schedules
def is_schedule_in_window(self, schedule):
"""
Checks if the schedule is within the scheduling window.
"""
now_unix = self.get_virtual_now()
window_start = self.config.get("scheduling_window_start")
window_end = self.config.get("scheduling_window_end")
if schedule.start_unix - window_start < now_unix and \
schedule.start_unix - window_end > now_unix:
return True
return False
def queue_programme(self):
"""
Queues the current programme (playlists as per schedule) by creating
timed commands to the sound-system to enable the individual tracks of playlists.
"""
# Get a clean set of the schedules within the scheduling window
schedules = self.get_next_schedules()
schedules = self.filter_scheduling_window(schedules)
# Queue the schedules, their playlists and entries
if schedules:
for next_schedule in schedules:
playlist = self.fallback_manager.resolve_playlist(next_schedule)
self.queue_playlist_entries(next_schedule, playlist.entries, False, True)
# Queue the fade-out of the schedule
if not next_schedule.fadeouttimer:
self.queue_end_of_schedule(next_schedule, True)
self.logger.info(SimpleUtil.green("Finished queuing programme."))
def queue_startup_entries(self):
"""
Queues all entries after the one currently playing upon startup. Don't use
this method in any other scenario, as it doesn't respect the scheduling window.
"""
current_schedule = self.get_active_schedule()
# Queue the (rest of the) currently playing schedule upon startup
if current_schedule:
current_playlist = self.fallback_manager.resolve_playlist(current_schedule)
if current_playlist:
active_entry = self.get_active_entry()
# Finished entries
for entry in current_playlist.entries:
if active_entry == entry:
break
# Entry currently being played
if active_entry:
# Queue open entries for current playlist
rest_of_playlist = active_entry.get_next_entries(True)
self.queue_playlist_entries(current_schedule, rest_of_playlist, False, True)
# Store them for later reference
current_schedule.queued_entries = rest_of_playlist
def queue_playlist_entries(self, schedule, entries, fade_in, fade_out):
"""
Creates sound-system player commands for all playlist items to be executed at the scheduled time.
Args:
schedule (Schedule): The schedule this entries belong to
entries ([PlaylistEntry]): The playlist entries to be scheduled for playout
fade_in (Boolean): Fade-in at the beginning of the set of entries
fade_out (Boolean): Fade-out at the end of the set of entries
Returns:
(String): Formatted string to display playlist entries in log
"""
entry_groups = []
entry_groups.append([])
previous_entry = None
index = 0
# Mark entries which start after the end of their schedule or are cut
clean_entries = self.preprocess_entries(entries, True)
# Group/aggregate all filesystem entries, allowing them to be queued at once
for entry in clean_entries:
if previous_entry == None or \
(previous_entry != None and \
previous_entry.get_type() == entry.get_type() and \
entry.get_type() == ChannelType.FILESYSTEM):
entry_groups[index].append(entry)
else:
index += 1
entry_groups.append([])
entry_groups[index].append(entry)
previous_entry = entry
self.logger.info("Built %s entry group(s)" % len(entry_groups))
# Schedule function calls
do_queue_schedule_end = False
if len(clean_entries) > 0 and len(entry_groups) > 0:
for entries in entry_groups:
if not isinstance(entries, list):
raise ValueError("Invalid Entry Group: %s" % str(entries))
# Create timers for each entry group
self.set_entries_timer(entries, fade_in, fade_out)
# Store them for later reference
schedule.queued_entries = clean_entries
else:
self.logger.warn(SimpleUtil.red("Nothing to schedule ..."))
def set_entries_timer(self, entries, fade_in, fade_out):
"""
Creates timer for loading and playing one or multiple entries. Existing timers are
updated.
Args:
entries ([]): List of multiple filesystem entries, or a single entry of other types
"""
play_timer = self.is_something_planned_at_time(entries[0].start_unix)
now_unix = self.get_virtual_now()
diff = entries[0].start_unix - now_unix
# Play function to be called by timer
def do_play(entries):
self.logger.info(SimpleUtil.cyan("=== play('%s') ===" % EngineUtil.get_entries_string(entries)))
transition_type = TransitionType.INSTANT
if fade_in:
transition_type = TransitionType.FADE
if entries[-1].status != EntryPlayState.READY:
self.logger.critical(SimpleUtil.red("PLAY: For some reason the entry/entries are not yet ready to be played (Entries: %s)" % EngineUtil.get_entries_string(entries)))
# At this point it's too late to do any pro-active fallback handling. Is it? Wait for the silence detector to deal with it.
# TODO Observe the actual handling of this section and think about possible improvements.
self.soundsystem.play(entries[0], transition_type)
self.logger.info(self.get_ascii_programme())
if play_timer:
# Check if the Playlist IDs are different
if self.have_entries_changed(play_timer, entries):
# If not, stop and remove the old timer, create a new one
self.stop_timer(play_timer)
else:
# If the playlist entries do not differ => reuse the old timer and do nothing
self.logger.debug("Playlist Entry %s is already scheduled - no new timer created." % EngineUtil.get_entries_string(entries))
return
# If nothing is planned at given time, create a new timer
(entries[0].switchtimer, entries[0].loadtimer) = self.create_timer(diff, do_play, entries, switcher=True)
def have_entries_changed(self, timer, new_entries):
"""
Checks if the new entries and playlists are matching the existing queued ones,
or if they should be updated.
Args:
timer (CallFunctionTimer): The timer holding queued entries
new_entries ([PlaylistEntry]): The possibly updated entries
Returns:
(Boolean): `True` if it has changed
"""
old_entries = timer.entries
if old_entries[0].playlist and new_entries[0].playlist:
if old_entries[0].playlist.playlist_id != new_entries[0].playlist.playlist_id:
return True
if len(old_entries) != len(new_entries):
return True
for old_entry, new_entry in zip(old_entries, new_entries):
if old_entry.source != new_entry.source:
return True
return False
def preprocess_entries(self, entries, cut_oos):
"""
Analyses and marks entries which are going to be cut or excluded.
Args:
entries ([PlaylistEntry]): The playlist entries to be scheduled for playout
cut_oos (Boolean): If `True` entries which are 'out of schedule' are not returned
Returns:
([PlaylistEntry]): The list of processed playlist entries
"""
clean_entries = []
for entry in entries:
if entry.entry_start >= entry.playlist.schedule.schedule_end:
msg = "Filtered entry (%s) after end-of schedule (%s) ... SKIPPED" % (entry, entry.playlist.schedule)
self.logger.warn(SimpleUtil.red(msg))
entry.queue_state = EntryQueueState.OUT_OF_SCHEDULE
elif entry.end_unix > entry.playlist.schedule.end_unix:
entry.queue_state = EntryQueueState.CUT
else:
entry.queue_state = EntryQueueState.OKAY
if not entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE or not cut_oos:
clean_entries.append(entry)
return clean_entries
def queue_end_of_schedule(self, schedule, fade_out):
"""
Queues a soundsystem action to stop/fade-out the given schedule.
Args:
schedule (PlaylistEntry): The schedule
fade_out (Boolean): If the schedule should be faded-out
"""
schedule_end = schedule.schedule_end
schedule_end_unix = schedule.end_unix
now_unix = self.get_virtual_now()
fade_out_time = 0
# Stop function to be called when schedule ends
def do_stop(schedule):
last_entry = schedule.queued_entries[-1] # FIXME sometimes an issue with startup queues
self.logger.info(SimpleUtil.cyan("=== stop('%s') ===" % str(last_entry.playlist.schedule)))
transition_type = TransitionType.INSTANT
if fade_out:
transition_type = TransitionType.FADE
self.soundsystem.stop(last_entry, transition_type)
if fade_out == True:
fade_out_time = int(round(float(self.config.get("fade_out_time")))) #FIXME Use float
# Stop any existing fade-out timer
if schedule.fadeouttimer:
schedule.fadeouttimer.cancel()
self.message_timer.remove(schedule.fadeouttimer)
# Create timer to fade-out
start_fade_out = schedule_end_unix - now_unix - fade_out_time
# last_entry = schedule.queued_entries[-1]
schedule.fadeouttimer = self.create_timer(start_fade_out, do_stop, schedule, fadeout=True)
self.logger.info("Fading out schedule in %s seconds at %s | Schedule: %s" % (str(start_fade_out), str(schedule_end), schedule))
def fetch_new_programme(self):
"""
Fetch the latest programme from `AuraCalendarService` which stores it to the database.
After that, the programme is in turn loaded from the database and stored in `self.programme`.
"""
# Fetch programme from API endpoints
self.logger.debug("Trying to fetch new programe from API endpoints...")
acs = AuraCalendarService(self.config)
queue = acs.get_queue()
acs.start() # start fetching thread
response = queue.get() # wait for the end
self.logger.debug("... Programme fetch via API done!")
# Reset last successful fetch state
lsf = self.last_successful_fetch
self.last_successful_fetch = None
if response is None:
msg = SimpleUtil.red("Trying to load programme from Engine Database, because AuraCalendarService returned an empty response.")
self.logger.warning(msg)
elif type(response) is list:
self.programme = response
if self.programme is not None and len(self.programme) > 0:
self.last_successful_fetch = datetime.now()
self.logger.info(SimpleUtil.green("Finished fetching current programme from API"))
if len(self.programme) == 0:
self.logger.critical("Programme fetched from Steering/Tank has no entries!")
elif response.startswith("fetching_aborted"):
msg = SimpleUtil.red("Trying to load programme from database only, because fetching was being aborted from AuraCalendarService! Reason: ")
self.logger.warning(msg + response[16:])
else:
msg = SimpleUtil.red("Trying to load programme from database only, because of an unknown response from AuraCalendarService: " + response)
self.logger.warning(msg)
# Always load latest programme from the database
self.last_successful_fetch = lsf
self.load_programme_from_db()
self.logger.info(SimpleUtil.green("Finished loading current programme from database (%s schedules)" % str(len(self.programme))))
for schedule in self.programme:
self.logger.debug("\tSchedule %s with Playlist %s" % (str(schedule), str(schedule.playlist)))
def load_programme_from_db(self):
"""
Loads the programme from Engine's database and enables
them via `self.enable_entries(..)`. After that, the
current message queue is printed to the console.
"""
self.programme = Schedule.select_programme()
if not self.programme:
self.logger.critical(SimpleUtil.red("Could not load programme from database. We are in big trouble my friend!"))
return
def is_something_planned_at_time(self, given_time):
"""
Checks for existing timers at the given time.
"""
for t in self.message_timer:
if t.fadein or t.switcher:
if t.entries[0].start_unix == given_time:
return t
return False
def create_timer(self, diff, func, param, fadein=False, fadeout=False, switcher=False):
"""
Creates a new timer for timed execution of mixer commands.
Args:
diff (Integer): The difference in seconds from now, when the call should happen
func (Function): The function to call
param ([]): A schedule or list of entries
Returns:
(CallFunctionTimer, CallFunctionTimer): In case of a "switch" command, the switch and pre-roll timer is returned
(CallFunctionTimer): In all other cases only the timer for the command is returned
"""
if not fadein and not fadeout and not switcher or fadein and fadeout or fadein and switcher or fadeout and switcher:
raise ValueError("You have to call me with either fadein=true, fadeout=true or switcher=True")
if not isinstance(param, list) and not isinstance(param, Schedule):
raise ValueError("No list of entries nor schedule passed!")
t = CallFunctionTimer(diff=diff, func=func, param=param, fadein=fadein, fadeout=fadeout, switcher=switcher)
self.message_timer.append(t)
t.start()
if switcher:
# Pre-roll function to be called by timer
def do_preroll(entries):
try:
if entries[0].get_type() == ChannelType.FILESYSTEM:
self.logger.info(SimpleUtil.cyan("=== preroll_group('%s') ===" % EngineUtil.get_entries_string(entries)))
self.soundsystem.preroll_group(entries)
else:
self.logger.info(SimpleUtil.cyan("=== preroll('%s') ===" % EngineUtil.get_entries_string(entries)))
self.soundsystem.preroll(entries[0])
except LoadSourceException as e:
self.logger.critical(SimpleUtil.red("Could not pre-roll entries %s" % EngineUtil.get_entries_string(entries)), e)
# Pro-active fallback handling, avoiding the need of the silence detector kicking-in.
self.fallback_manager.handle_proactive_fallback(self, entries[0].playlist)
if entries[-1].status != EntryPlayState.READY:
self.logger.critical(SimpleUtil.red("Entries didn't reach 'ready' state during pre-rolling (Entries: %s)" % EngineUtil.get_entries_string(entries)))
# Pro-active fallback handling, avoiding the need of the silence detector kicking-in.
self.fallback_manager.handle_proactive_fallback(self, entries[0].playlist)
loader_diff = diff - self.config.get("preroll_offset")
loader = CallFunctionTimer(diff=loader_diff, func=do_preroll, param=param, fadein=fadein, fadeout=fadeout, switcher=False, loader=True)
self.message_timer.append(loader)
loader.start()
return (t, loader)
else:
return t
def stop_timer(self, timer):
"""
Stops the given timer.
Args:
timer (Timer): The timer to stop.
"""
timer.cancel()
count = 1
for entry in timer.entries:
if entry.loadtimer is not None:
entry.loadtimer.cancel()
self.message_timer.remove(entry.loadtimer)
count += 1
# if timer.entries[0].fadeintimer is not None:
# timer.entries[0].fadeintimer.cancel()
# self.message_timer.remove(timer.entries[0].fadeintimer)
# count += 1
# if entry.fadeouttimer is not None:
# entry.fadeouttimer.cancel()
# self.message_timer.remove(entry.fadeouttimer)
# count += 1
# Remove it from message queue
self.message_timer.remove(timer)
self.logger.info("Stopped %s timers for: %s" % (str(count), EngineUtil.get_entries_string(timer.entries)))
# FIXME Move to adequate module
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.is_empty()
except sqlalchemy.exc.ProgrammingError as e:
errcode = e.orig.args[0]
if errcode == 1146: # Error for no such table
model = AuraDatabaseModel()
model.recreate_db()
else:
raise
def terminate(self):
"""
Called when thread is stopped or a signal to terminate is received.
"""
self.exit_event.set()
self.logger.info("Shutting down scheduler ...")
# ------------------------------------------------------------------------------------------ #
# 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
entries = None
diff = None
dt = None
fadein = False
fadeout = False
switcher = False
loader = False
def __init__(self, diff=None, func=None, param=None, fadein=False, fadeout=False, switcher=False, loader=False):
self.logger = logging.getLogger("AuraEngine")
self.logger.debug("Executing soundsystem command '%s' in %s seconds..." % (str(func.__name__), str(diff)))
threading.Timer.__init__(self, diff, func, (param,))
if not fadein and not fadeout and not switcher and not loader \
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.dt = datetime.now() + timedelta(seconds=diff)
self.func = func
self.param = param
self.entries = param # TODO Refactor since param can hold [entries] or a schedule, depending on the timer type
self.fadein = fadein
self.fadeout = fadeout
self.switcher = switcher
self.loader = loader
def __str__(self):
"""
String represenation of the timer.
"""
status = "Timer (Alive: %s)" % self.is_alive()
status += " starting at " + str(self.dt)
if self.fadein:
return status + " fading in entries '" + EngineUtil.get_entries_string(self.entries)
elif self.fadeout:
return status + " fading out schedule '" + str(self.param)
elif self.switcher:
return status + " switching to entries '" + EngineUtil.get_entries_string(self.entries)
elif self.loader:
return status + " pre-rolling entries '" + EngineUtil.get_entries_string(self.entries)
else:
return "CORRUPTED CallFunctionTimer around! How can that be?"
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from enum import Enum
class PlaylistType(Enum):
"""
Types of playlists.
"""
DEFAULT = { "id": 0, "name": "default" } # Default play mode
SHOW = { "id": 1, "name": "show" } # The first played when some default playlist fails
TIMESLOT = { "id": 2, "name": "timeslot" } # The second played when the timeslot fallback fails
STATION = { "id": 3, "name": "station" } # The last played when everything else fails
@property
def id(self):
return self.value["id"]
def __str__(self):
return str(self.value["name"])
# class TimerType(Enum):
# """
# Types of queue timers.
# """
# SWITCH = "switch"
# FADEIN = "fadein"
# FADEOUT = "fadeout"
class EntryQueueState(Enum):
"""
Types of playlist entrie behaviours.
"""
OKAY = "ok"
CUT = "cut"
OUT_OF_SCHEDULE = "oos"
sqlalchemy==1.3.13
Flask==1.1.1
Flask-Caching==1.8.0
Flask-SQLAlchemy==2.4.1
Flask-RESTful==0.3.8
flask-marshmallow==0.11.0
flask-cors==3.0.8
marshmallow-sqlalchemy==0.22.2
apispec==3.3.0
apispec-webframeworks==0.5.2
mysqlclient==1.3.12
redis==3.5.3
mutagen==1.44.0
validators==0.12.1
simplejson==3.17.0
accessify==0.3.1
librosa==0.7.2
gunicorn==20.0.4
pyyaml==5.3.1
\ No newline at end of file
#!/bin/bash
mode="engine"
docker="false"
#
# Run Script for AURA Engine
#
# Call with one of these parameters:
#
# - engine
# - core
# - lqs
# - api-dev
# - api
# - recreate-database
# - docker:engine
# - docker:core
# - docker:lqs
# - docker:recreate-database
# - docker:build
# - docker:api
#
if [[ $* =~ ^(engine|core|lqs|api-dev|api)$ ]]; then
mode=$1
fi
if [[ "$1" == *"docker:"* ]]; then
docker="true"
mode=${1#*:}
fi
echo "[ Run mode=$mode ]"
echo "[ Docker=$docker ]"
# +++ DEFAULT COMMANDS +++ #
if [[ $docker == "false" ]]; then
### Runs Engine Core & Liquidsoap ###
if [[ $mode == "engine" ]]; then
/usr/bin/env python3.7 engine-core.py
fi
### Runs Engine Core only ###
if [[ $mode == "core" ]]; then
/usr/bin/env python3.7 engine-core.py --without-lqs
fi
### Runs Liquidsoap only ###
if [[ $mode == "lqs" ]]; then
lqs=$(/usr/bin/env python3.7 engine-core.py --get-lqs-command)
eval "$lqs"
fi
### Runs the API Server (Development) ###
if [[ $mode == "api-dev" ]]; then
echo "Building Web Applications"
sh ./script/build-web.sh
echo "Starting API Server"
/usr/bin/env python3.7 engine-api.py
fi
### Runs the API Server (Production) ###
if [[ $mode == "api" ]]; then
echo "Activating Python Environment"
source ../python-env/bin/activate
echo "Starting API Server"
gunicorn -c configuration/gunicorn.conf.py engine-api:app
fi
### CAUTION: This deletes everything in your database ###
if [[ $mode == "recreate-database" ]]; then
/usr/bin/env python3.7 engine-core.py --recreate-database
fi
fi
# +++ DOCKER COMMANDS +++ #
if [[ $docker == "true" ]]; then
BASE_D=$(realpath "${BASH_SOURCE%/*}/")
### Runs Engine Core & Liquidsoap ###
if [[ $mode == "engine" ]]; then
exec sudo docker run --name aura-engine --rm -it \
-u $UID:$GID \
-p 127.0.0.1:8050:5000 \
-v "$BASE_D":/srv \
-v "$BASE_D/configuration/":/etc/aura \
--tmpfs /var/log/aura/ autoradio/engine /srv/engine-core.py
fi
### Runs Engine Core only ###
if [[ $mode == "core" ]]; then
exec sudo docker run --name aura-engine-core --rm -it \
-u $UID:$GID \
-p 127.0.0.1:8050:5000 \
-v "$BASE_D":/srv \
-v "$BASE_D/configuration/":/etc/aura \
--tmpfs /var/log/aura/ autoradio/engine /srv/engine-core.py "--without-lqs"
fi
### Runs Liquidsoap only ###
if [[ $mode == "lqs" ]]; then
lqs=$(/usr/bin/env python3.7 engine-core.py --get-lqs-command)
eval "$lqs"
exec sudo docker run --name aura-engine-liquidsoap --rm -it \
-u 1000:1000 \
-v "$BASE_D":/srv \
-v "$BASE_D/configuration/":/etc/aura \
--tmpfs /var/log/aura/ \
--device /dev/snd autoradio/engine /bin/bash \
-c "cd /srv/modules/liquidsoap && liquidsoap --debug --verbose engine.liq"
fi
### Runs Engine API using Gunicorn ###
if [[ $mode == "api" ]]; then
exec sudo docker run --name aura-engine-api --rm -it \
-u $UID:$GID \
-p 127.0.0.1:8050:5000 \
-v "$BASE_D":/srv \
-v "$BASE_D/configuration/":/etc/aura \
--tmpfs /var/log/aura/ autoradio/engine /srv/engine-api.py \
--device autoradio/engine /bin/bash \
-c "gunicorn -c configuration/gunicorn.conf.py engine-api:app"
fi
### CAUTION: This deletes everything in your database ###
if [[ $mode == "recreate-database" ]]; then
exec sudo docker run --rm -it \
-u $UID:$GID \
-v "$BASE_D":/srv \
-v "$BASE_D/configuration/":/etc/aura \
--tmpfs /var/log/aura/ autoradio/engine /srv/engine-core.py --recreate-database
fi
### Create Docker Image from local project ###
if [[ $mode == "build" ]]; then
exec sudo docker build -t autoradio/engine .
fi
### Pushes the latest Docker Image to Docker Hub ###
if [[ $mode == "push" ]]; then
exec sudo docker push autoradio/engine
fi
fi
\ No newline at end of file
#!/bin/bash
echo "Building AURA Clock ..."
(
cd contrib/aura-clock
npm run build
)
cp contrib/aura-clock/public/build/aura-clock-bundle.css web/css/aura-clock-bundle.css
cp contrib/aura-clock/public/build/aura-clock-bundle.js web/js/aura-clock-bundle.js
echo "Building AURA Player ..."
(
cd contrib/aura-player
npm run build
)
cp contrib/aura-player/public/build/aura-player-bundle.css web/css/aura-player-bundle.css
cp contrib/aura-player/public/build/aura-player-bundle.js web/js/aura-player-bundle.js
#!/bin/bash
if getent passwd 'engineuser' > /dev/null 2>&1; then
echo "User 'engineuser' exists already.";
else
echo "Creating Engine User ..."
adduser engineuser
adduser engineuser audio
fi
\ No newline at end of file
#!/bin/bash
#
# Prepare folders and permissions for installing engine on production.
#
# You'll need sudo/root privileges.
#
echo "Create Log Directory '/var/log/aura/'"
mkdir -p /var/log/aura
echo "Create Configuration Directory '/etc/aura/'"
mkdir -p /etc/aura
echo "Set Ownership of '/opt/aura/engine', '/var/log/aura/' and '/etc/aura/engine.ini' to Engine User"
chown -R engineuser:engineuser /opt/aura
chown -R engineuser:engineuser /etc/aura
chown -R engineuser:engineuser /var/log/aura
chown -R engineuser:engineuser /var/log/supervisor
echo "Copy Systemd unit files to '/etc/systemd/system/'"
cp -n /opt/aura/engine/configuration/systemd/* /etc/systemd/system/
\ No newline at end of file
#!/bin/bash
opam update -y
opam init -y
opam switch create 4.08.0
opam install depext -y
opam depext taglib mad lame vorbis flac opus cry samplerate pulseaudio bjack alsa ssl liquidsoap -y
opam install taglib mad lame vorbis flac opus cry samplerate pulseaudio bjack alsa ssl liquidsoap -y
eval $(opam env)
#!/bin/bash
echo "Installing AURA Clock Packages ..."
(cd contrib/aura-clock && npm install)
echo "Installing AURA Player Packages ..."
(cd contrib/aura-player && npm install)
#!/bin/bash
kill -9 `ps -eo pid,command | grep 'gunicorn.*engine-api:run_app()' | grep -v grep | sort | head -1 | awk '{print $1}'`
\ No newline at end of file
#!/bin/bash
# Check if databases are already set-up
if test -f "$LOCKFILE_DB"; then
echo "Aura Engine Databases are already existing! Skipping..."
else
# Create random password
PASS_ENGINE="$(openssl rand -base64 24)"
# Create databases and users
echo "--- SETTING UP DATABASE AND USERS ---"
echo "Please enter the MySQL/MariaDB root password!"
stty -echo
printf "Password: "
read rootpasswd
stty echo
printf "\n"
echo "---"
echo "Creating database for Aura Engine..."
mysql -uroot -p${rootpasswd} -e "CREATE DATABASE aura_engine CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -uroot -p${rootpasswd} -e "CREATE USER 'aura'@'localhost' IDENTIFIED BY '${PASS_ENGINE}';"
mysql -uroot -p${rootpasswd} -e "GRANT ALL PRIVILEGES ON aura_engine.* TO 'aura'@'localhost';"
mysql -uroot -p${rootpasswd} -e "FLUSH PRIVILEGES;"
echo "Done."
echo
echo
echo "Please note your database credentials for the next configuration steps:"
echo "-----------------------------------------------------------------------"
echo " Database: 'aura_engine'"
echo " User: 'aura'"
echo " Password: '${PASS_ENGINE}'"
echo "-----------------------------------------------------------------------"
echo
fi
\ No newline at end of file
#!/bin/bash
#
# Setup Database
#
# Set LOCK file location
LOCKFILE_DB=.engine.install-db.lock
# Check if databases are already set-up
if test -f "$LOCKFILE_DB"; then
echo "Aura Engine Databases are already existing! Skipping..."
else
echo "Setting up database ..."
echo
echo "Which database system do you want to use? (Press '1' or '2')"
echo " [1] MariaDB"
echo " [2] Other / Manually"
echo
while true; do
read -rsn1 input
if [ "$input" = "1" ]; then
echo "Creating DB for MariaDB ..."
bash script/setup-db-mariadb.sh
break
fi
if [ "$input" = "2" ]; then
echo "Manual database setup selected."
break
fi
done
# Create lockfile to avoid accidential re-creation of the database
touch $LOCKFILE_DB
fi
; supervisor config file
[unix_http_server]
file=/opt/aura/engine/tmp/supervisor.sock ; (the path to the socket file)
chmod=0700 ; sockef file mode (default 0700)
chown=engineuser:engineuser
[supervisord]
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
pidfile=/opt/aura/engine/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP)
; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///opt/aura/engine/tmp/supervisor.sock ; use a unix:// URL for a unix socket
; The [include] section can just contain the "files" setting. This
; setting can list multiple files (separated by whitespace or
; newlines). It can also contain wildcards. The filenames are
; interpreted as relative to this file. Included files *cannot*
; include files themselves.
[include]
; files = /etc/supervisor/conf.d/*.conf
files = /opt/aura/engine/configuration/supervisor/*.conf
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urllib
import json
from modules.core.engine import SoundSystem
from modules.base.config import AuraConfig
# ------------------------------------------------------------------------------------------ #
class ConnectionTester(AuraConfig):
# ------------------------------------------------------------------------------------------ #
def __init__(self):
super(ConnectionTester, self).__init__()
# ------------------------------------------------------------------------------------------ #
def get_connection_status(self):
status = dict()
status["db"] = False # self.test_db_conn()
status["pv"] = self.test_pv_conn()
status["lqs"] = self.test_lqs_conn()
status["lqsr"] = False # self.test_lqsr_conn()
status["tank"] = self.test_tank_conn()
status["redis"] = self.test_redis_conn()
return json.dumps(status)
# ------------------------------------------------------------------------------------------ #
# def test_db_conn(self):
# try:
# ScheduleEntry.select_all()
# except:
# return False
#
# return True
# ------------------------------------------------------------------------------------------ #
def test_lqs_conn(self):
try:
lsc = SoundSystem(self.config)
lsc.mixer_status()
return True
except Exception as e:
return False
# ------------------------------------------------------------------------------------------ #
def test_lqsr_conn(self):
try:
lsc = SoundSystem(self.config)
lsc.get_recorder_status()
return True
except Exception as e:
return False
# ------------------------------------------------------------------------------------------ #
def test_pv_conn(self):
return self.test_url_connection(self.config.get("calendarurl"))
# ------------------------------------------------------------------------------------------ #
def test_tank_conn(self):
# test load of playlist 1
return self.test_url_connection(self.config.get("importerurl")+"1")
# ------------------------------------------------------------------------------------------ #
def test_redis_conn(self):
from modules.cli.redis.adapter import ClientRedisAdapter
try:
cra = ClientRedisAdapter()
cra.publish("aura", "status")
except:
return False
return True
def test_url_connection(self, url):
try:
request = urllib.request.Request(url)
response = urllib.request.urlopen(request)
response.read()
except Exception as e:
return False
return True
\ No newline at end of file
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
File added
set("log.file.path", "./<script>.log")
#%include "readini.liq"
#ini = read_ini("/etc/aura/engine.ini")
# output_source = mksafe(blank())
# %include "stream.liq"
# stream = get_stream(0)
# output_stream = input.external("arecord -f S16_LE -c2 -r44100 -t raw -D dsnoop:1,0 -")
output.icecast(
%vorbis(quality = 0.5),
mount="aura-test.ogg",
host="develop.servus.at",
port=8000,
name="LQSTest",
user="source",
password="A7E7tst1",
fallible=true,
input.alsa(device="pcm.plugj")
)
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import unittest
import validators
from datetime import datetime
from modules.base.logger import AuraLogger
from modules.base.config import AuraConfig
from modules.base.models import Schedule, TrackService
# modules
from modules.core.engine import SoundSystem
from modules.scheduling.scheduler import AuraScheduler
class TestLogger(unittest.TestCase):
aura_logger = None
def setUp(self):
self.config = AuraConfig()
self.aura_logger = AuraLogger(self.config)
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.config.ini_path, "/etc/aura/engine.ini")
# install_dir is set by runtime. is it a directory?
self.assertTrue(os.path.isdir(self.config.get("install_dir")))
# calendarurl and importerurl set and valid urls?
self.assertTrue(validators.url(self.config.get("calendarurl")))
self.assertTrue(validators.url(self.config.get("importerurl")))
# is liquidsoap socketdir set and a directory?
self.assertTrue(os.path.isdir(self.config.get("socketdir")))
# database settings set?
self.assertIsNotNone(self.config.get("db_user"))
self.assertIsNotNone(self.config.get("db_pass"))
self.assertIsNotNone(self.config.get("db_name"))
self.assertIsNotNone(self.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.soundsystem = SoundSystem(p)
self.soundsystem.scheduler = AuraScheduler(p)
self.soundsystem.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
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
set("log.file.path", "./<script>.log")
set("server.telnet", true)
set("server.telnet.bind_addr", "0.0.0.0")
set("server.telnet.port", 1234)
# ALSA / pulse settings
# durch ausprobieren herausgefunden für asus xonar dgx 5.1
# chip: CMI8788
# driver: snd_oxygen
set("frame.duration", 0.30)
set("alsa.alsa_buffer", 8192) # 7168) # 6144) # 8192) # 10240) #15876
set("alsa.buffer_length", 25)
set("alsa.periods", 0) # assertion error when setting periods other than 0 => alsa default
input_linein = input.alsa(id="linein", bufferize = false)
#input_fs = single(id="fs", "/var/audio/fallback/output.flac")
#input_http = input.http(id="http", "http://stream.fro.at/fro-128.ogg")
#mixer = mix(id="mixer", [input_fs, input_http, input_linein])
#output.alsa(id="lineout", bufferize = false, mixer)
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 David Trattnig <david@subsquare.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
set("log.file.path", "./<script>.log")
set("server.telnet", true)
set("server.telnet.bind_addr", "0.0.0.0")
set("server.telnet.port", 1234)
set("frame.duration", 0.30)
set("alsa.alsa_buffer", 8192)
set("alsa.buffer_length", 25)
set("alsa.periods", 0) # assertion error when setting periods other than 0 => alsa default
#input_linein = input.alsa(id="linein", bufferize = false)
audio1 = single(id="fs1", "./sources/1.flac")
audio2 = single(id="fs2", "./sources/2.flac")
#input_http = input.http(id="http", "http://stream.fro.at/fro-128.ogg")
#mixer = mix(id="mixer", [audio1, audio2])
#mixer = mix(id="mixer", [input_fs, input_http, input_linein])
mixed = add([audio1, audio2])
output.alsa(id="lineout", bufferize = false, mixed)