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 2619 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 requests
from datetime import datetime
from modules.base.utils import SimpleUtil as SU
class CalendarFetcher:
"""
Fetches the schedules, playlists and playlist entries as JSON
via the API endpoints of Steering and Tank.
"""
config = None
logging = None
has_already_fetched = False
fetched_schedule_data = None
# Config for API Endpoints
steering_calendar_url = None
tank_playlist_url = None
tank_session = None
tank_secret = None
def __init__(self, config):
"""
Constructor
Args:
config (AuraConfig): Holds the engine configuration
"""
self.config = config
self.logger = logging.getLogger("AuraEngine")
self.steering_calendar_url = self.config.get("api_steering_calendar")
self.tank_playlist_url = self.config.get("api_tank_playlist")
self.tank_session = self.config.get("api_tank_session")
self.tank_secret = self.config.get("api_tank_secret")
#
# PUBLIC METHODS
#
def fetch(self):
"""
Retrieve all required data from the API.
"""
return_data = []
self.logger.debug("Fetching schedules from STEERING")
self.fetched_schedule_data = self.fetch_schedule_data()
if not self.fetched_schedule_data:
self.logger.critical(SU.red("No schedules fetched from API!"))
return None
self.logger.debug("Fetching playlists from TANK")
self.fetch_playlists()
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 \
and "show_fallback" not in schedule \
and "schedule_fallback" not in schedule \
and "station_fallback" 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:
self.logger.error(SU.red("Nothing fetched ..."))
self.fetched_schedule_data = None
return None
return return_data
#
# PRIVATE METHODS
#
def fetch_schedule_data(self):
"""
Fetches schedule data from Steering.
Returns:
([Schedule]): An array of schedules
"""
schedule = None
headers = { "content-type": "application/json" }
try:
self.logger.debug("Fetch schedules from Steering API...")
response = requests.get(self.steering_calendar_url, data=None, headers=headers)
if not response.status_code == 200:
self.logger.critical(SU.red("HTTP Status: %s | Schedules could not be fetched! Response: %s" % \
(str(response.status_code), response.text)))
return None
schedule = response.json()
except Exception as e:
self.logger.critical(SU.red("Error while requesting schedules from Steering!"), e)
if not schedule:
self.logger.error(SU.red("Got no schedule via Playout API (Steering)!"))
return None
return self.remove_unnecessary_data(schedule)
def fetch_playlists(self):
"""
Fetches all playlists including fallback playlists for every schedule.
This method used the class member `fetched_schedule_data`` to iterate
over and extend schedule data.
"""
# store fetched entries => do not have to fetch playlist_id more than once
fetched_entries=[]
local_station_fallback_id = str(self.config.get("scheduling_station_fallback_id"))
try:
for schedule in self.fetched_schedule_data:
# Get IDs of playlists
playlist_id = self.get_playlist_id(schedule, "playlist_id")
schedule_fallback_id = self.get_playlist_id(schedule, "schedule_fallback_id")
show_fallback_id = self.get_playlist_id(schedule, "show_fallback_id")
station_fallback_id = self.get_playlist_id(schedule, "station_fallback_id")
# Retrieve playlist and the fallback playlists for every schedule.
# If a playlist (like station_fallback) is already fetched, it is not fetched again but reused
schedule["playlist"] = self.fetch_playlist(playlist_id, fetched_entries)
schedule["schedule_fallback"] = self.fetch_playlist(schedule_fallback_id, fetched_entries)
schedule["show_fallback"] = self.fetch_playlist(show_fallback_id, fetched_entries)
schedule["station_fallback"] = self.fetch_playlist(station_fallback_id, fetched_entries)
# If Steering doesn't provide a station fallback, the local one is used
if not schedule["station_fallback"] and int(local_station_fallback_id) > 0:
schedule["station_fallback_id"] = local_station_fallback_id
schedule["station_fallback"] = self.fetch_playlist(local_station_fallback_id, fetched_entries)
if schedule["station_fallback"]:
self.logger.info("Assigned playlist #%s as local station fallback to schedule #%s" % (local_station_fallback_id, schedule["schedule_id"]))
except Exception as e:
self.logger.error("Error while fetching playlists from API endpoints: " + str(e), e)
def fetch_playlist(self, playlist_id, fetched_playlists):
"""
Fetches the playlist for a given schedule.
Args:
id_name (String): The type of playlist to fetch (e.g. normal vs. fallback)
fetched_playlists ([]): Previously fetched playlists to avoid re-fetching
Returns:
(Playlist): Playlist of type `id_name`
"""
if not playlist_id:
return None
playlist = None
url = self.tank_playlist_url.replace("${ID}", playlist_id)
headers = {
"Authorization": "Bearer %s:%s" % (self.tank_session, self.tank_secret),
"content-type": "application/json"
}
# If playlist is already fetched in this round, use the existing one
for playlist in fetched_playlists:
if playlist["id"] == playlist_id:
self.logger.debug("Playlist #%s already fetched" % playlist_id)
return playlist
try:
self.logger.debug("Fetch playlist from Tank API...")
response = requests.get(url, data=None, headers=headers)
if not response.status_code == 200:
self.logger.critical(SU.red("HTTP Status: %s | Playlist #%s could not be fetched or is not available! Response: %s" % \
(str(response.status_code), str(playlist_id), response.text)))
return None
playlist = response.json()
except Exception as e:
self.logger.critical(SU.red("Error while requesting playlist #%s from Tank" % str(playlist_id)), e)
return None
fetched_playlists.append(playlist)
return playlist
def get_playlist_id(self, schedule, id_name):
"""
Extracts the playlist ID for a given playlist (fallback) type.
"""
playlist_id = str(schedule[id_name])
if not playlist_id or playlist_id == "None":
self.logger.debug("No value defined for '%s' in schedule '#%s'" % (id_name, schedule["schedule_id"]))
return None
return playlist_id
def remove_unnecessary_data(self, schedule):
"""
Removes all schedules which are not relevant for
further processing.
"""
count_before = len(schedule)
schedule = self.remove_data_more_than_24h_in_the_future(schedule)
schedule = self.remove_data_in_the_past(schedule)
count_after = len(schedule)
self.logger.debug("Removed %d unnecessary schedules from response. Entries left: %d" % ((count_before - count_after), count_after))
return schedule
def remove_data_more_than_24h_in_the_future(self, schedules):
"""
Removes entries 24h in the future and 12 hours in the past.
Note: This might influence resuming (in case of a crash)
single schedules which are longer than 12 hours long.
Think e.g. live broadcasts.
"""
items = []
now = SU.timestamp()
now_plus_24hours = now + (12*60*60)
now_minus_12hours = now - (12*60*60)
for s in schedules:
start_time = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S")
start_time = SU.timestamp(start_time)
if start_time <= now_plus_24hours and start_time >= now_minus_12hours:
items.append(s)
return items
def remove_data_in_the_past(self, schedules):
"""
Removes all schedules from the past, except the one which is
currently playing.
"""
items = []
now = SU.timestamp()
for s in schedules:
start_time = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S")
start_time = SU.timestamp(start_time)
end_time = datetime.strptime(s["end"], "%Y-%m-%dT%H:%M:%S")
end_time = SU.timestamp(end_time)
# Append all elements in the future
if start_time >= now:
items.append(s)
# Append the one which is playing now
elif start_time < now < end_time:
items.append(s)
return items
#
# 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, os.path
import random
from accessify import private, protected
from modules.scheduling.types import PlaylistType
from modules.base.utils import SimpleUtil, EngineUtil
from modules.base.mail import AuraMailer
from modules.core.channels import ChannelType
class FallbackManager:
"""
Handles all types of fallbacks in case there is an outage
for the regular radio programme.
Attributes:
config (AuraConfig): The engine configuration
logger (AuraLogger): The logger
mail (AuraMailer): Mail service
scheduler (AuraScheduler): The scheduler
fallback_history (Dict): Holds a 24h history of played, local tracks to avoid re-play
last_fallback (Integer): Timestamp, when the last local file fallback was played
is_processing (Boolean): Flag to avoid race-conditions, as Liquidsoap sends plenty of requests at once
"""
config = None
logger = None
mailer = None
scheduler = None
fallback_history = {}
last_fallback = 0
is_processing = False
def __init__(self, config, logger, scheduler):
"""
Constructor
Args:
config (AuraConfig): Holds the engine configuration
"""
self.config = config
self.logger = logger
self.mailer = AuraMailer(self.config)
self.scheduler = scheduler
self.logger = logger
#
# PUBLIC METHODS
#
def resolve_playlist(self, schedule):
"""
Resolves the (fallback) playlist for the given schedule in case of pro-active fallback scenarios.
A resolved playlist represents the state how it would currently be aired. For example the `FallbackManager`
evaluated, that the actually planned playlist cannot be played for various reasons (e.g. entries n/a).
Instead one of the fallback playlists should be played. If the method is called some time later,
it actually planned playlist might be valid, thus returned as the resolved playlist.
As long the adressed schedule is still within the scheduling window, the resolved playlist can
always change.
This method also updates `schedule.fallback_state` to the current fallback type (`PlaylistType`).
Args:
schedule (Schedule): The schedule to resolve the playlist for
Returns:
(Playlist): The resolved playlist
"""
playlist = None
type = None
self.logger.info("Resolving playlist for schedule #%s ..." % schedule.schedule_id)
if not self.validate_playlist(schedule, "playlist"):
if not self.validate_playlist(schedule, "schedule_fallback"):
if not self.validate_playlist(schedule, "show_fallback"):
if not self.validate_playlist(schedule, "station_fallback"):
self.logger.error(SimpleUtil.red("No (fallback) playlists for schedule #%s available - not even a single one!" % schedule.schedule_id))
return None
else:
type = PlaylistType.STATION
playlist = schedule.station_fallback
else:
type = PlaylistType.TIMESLOT
playlist = schedule.schedule_fallback
else:
type = PlaylistType.SHOW
playlist = schedule.show_fallback
else:
type = PlaylistType.DEFAULT
playlist = schedule.playlist
if type and type != PlaylistType.DEFAULT:
previous_type = schedule.fallback_state
if type == previous_type:
self.logger.info("Fallback state for schedule #%s is still '%s'" % (schedule.schedule_id, type))
else:
self.logger.warn("Detected fallback type switch from '%s' to '%s' is required for schedule %s." % (previous_type, type, str(schedule)))
schedule.fallback_state = type
return playlist[0]
def handle_proactive_fallback(self, scheduler, playlist):
"""
This is the 1st level strategy for fallback handling. When playlist entries are pre-rolled their
state is validated. If any of them doesn't become "ready to play" in time, some fallback entries
are queued.
"""
resolved_playlist = self.resolve_playlist(playlist.schedule)
if playlist != resolved_playlist:
self.logger.info("Switching from playlist #%s to fallback playlist #%s ..." % (playlist.playlist_id, resolved_playlist.playlist_id))
# Destroy any existing queue timers
for entry in playlist.entries:
scheduler.stop_timer(entry.switchtimer)
self.logger.info("Stopped existing timers for entries")
# Queue the fallback playlist
scheduler.queue_playlist_entries(resolved_playlist.schedule, resolved_playlist.entries, False, True)
self.logger.info("Queued fallback playlist entries (Fallback type: %s)" % playlist.type)
else:
self.logger.critical(SimpleUtil.red("For some strange reason the fallback playlist equals the currently failed one?!"))
def get_fallback_for(self, fallbackname):
"""
Retrieves a random fallback audio source for any of the types:
- timeslot/schedule
- show
- station
Args:
fallbackname (String): Fallback type
Returns:
(String): Absolute path to the file
"""
file = ""
media_type = "PLAYLIST"
active_schedule, active_playlist = self.scheduler.get_active_playlist()
# Block access to avoid race-conditions
if self.is_processing:
return None
else:
self.is_processing = True
# Get fallback track(s) by fallback-type
if fallbackname == "timeslot":
file = self.get_playlist_items(active_schedule, "schedule_fallback")
elif fallbackname == "show":
file = self.get_playlist_items(active_schedule, "show_fallback")
elif fallbackname == "station":
file = self.get_playlist_items(active_schedule, "station_fallback")
if not file:
media_type = "TRACK"
file = self.get_random_local_track()
if not file:
self.logger.critical("Got no file for station fallback! Playing default test track, to play anything at all.")
file = "../../test/content/ernie_mayne_sugar.mp3"
media_type = "DEFAULT TRACK"
else:
file = ""
self.logger.critical("Should set next fallback file for " + fallbackname + ", but this fallback is unknown!")
if file:
# Send admin email to notify about the fallback state
if not active_playlist:
active_playlist = "-"
msg = "AURA ENGINE %s FALLBACK DETECTED!\n\n" % fallbackname
msg += "Expected, active Schedule: %s \n" % active_schedule
msg += "Expected, active Playlist: %s \n\n" % active_playlist
msg += "Providing FALLBACK-%s for %s '%s'\n\n" % (media_type, fallbackname, file)
msg += "Please review the schedules or contact your Aura Engine administrator."
self.mailer.send_admin_mail("CRITICAL - Detected fallback for %s" % fallbackname, msg)
self.logger.warn("Providing fallback %s: '%s'. Sent admin email about fallback state" % (media_type, file))
self.is_processing = False
return file
def fallback_has_started(self, artist, title):
"""
Called when a fallback track has actually started playing
"""
self.logger.info("Now playing: fallback track '%s - %s'." % (artist, title))
#
# PRIVATE METHODS
#
def validate_playlist(self, schedule, playlist_type):
"""
Checks if a playlist is valid for play-out.
"""
playlist = getattr(schedule, playlist_type)
if playlist \
and isinstance(playlist, list) \
and playlist[0].entries \
and len(playlist[0].entries) > 0:
return True
return False
def validate_entries(self, entries):
"""
Checks if playlist entries are valid for play-out.
"""
for entry in entries:
if entry.get_type() == ChannelType.FILESYSTEM:
audio_store = self.config.get("audiofolder")
filepath = EngineUtil.uri_to_filepath(audio_store, entry.source)
if not self.is_audio_file(filepath):
self.logger.warn("Invalid filesystem path '%s' in entry '%s'" % (filepath, str(entry)))
return False
return True
def get_playlist_items(self, schedule, fallback_key):
"""
Retrieves the list of tracks from a playlist defined by `fallback_key`.
"""
playlist_files = ""
if hasattr(schedule, fallback_key):
playlist = getattr(schedule, fallback_key)
if len(playlist) > 0:
playlist = playlist[0]
if playlist and playlist.entries:
for entry in playlist.entries:
playlist_files += entry.source + "\n"
return playlist_files
def get_random_local_track(self):
"""
Retrieves a random audio track from the local station-fallback directory.
Returns:
(String): Absolute path to an audio file
"""
dir = self.config.fallback_music_folder
files = os.listdir(dir)
audio_files = list(filter(lambda f: self.is_audio_file(os.path.join(dir, f)), files))
if not dir or not audio_files:
self.logger.error("Folder 'fallback_music_folder = %s' is empty!" % dir)
return None
# If last played fallback is > 24 hours ago, ignore play history
# This should save used memory if the engine runs for a long time
if self.last_fallback < SimpleUtil.timestamp() - (60*60*24):
self.fallback_history = {}
self.logger.info("Cleared fallback history.")
self.last_fallback = SimpleUtil.timestamp()
# Retrieve files which haven't been played yet
history = set(self.fallback_history.keys())
left_audio_files = list( set(audio_files) - (history) )
self.logger.info("Left fallback audio-files: %d/%d" % (len(left_audio_files), len(audio_files)))
# If nothing left, clear history and start with all files again
if not len(left_audio_files):
self.fallback_history = {}
left_audio_files = audio_files
# Select random track from directory
i = random.randint(0, len(left_audio_files)-1)
file = os.path.join(dir, left_audio_files[i])
# Store track in history, to avoid playing it multiple times
if file:
self.fallback_history[left_audio_files[i]] = SimpleUtil.timestamp()
return file
def is_audio_file(self, file):
"""
Checks if the passed file is an audio file i.e. has a file-extension
known for audio files.
Args:
dir (String):
file (File): the file object.
Returns:
(Boolean): True, if it's an audio file.
"""
audio_extensions = [".wav", ".flac", ".mp3", ".ogg", ".m4a"]
ext = os.path.splitext(file)[1]
if os.path.isfile(file):
if any(ext in s for s in audio_extensions):
return True
return False
\ No newline at end of file
#
# 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
def get_active_playlist(self):
"""
Retrieves the currently playing playlist.
Returns:
(Playlist): The resolved playlist
"""
schedule = self.get_active_schedule()
playlist = self.fallback_manager.resolve_playlist(schedule)
return playlist
# 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)
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 fallback playlist below ↓↓↓")
if resolved_playlist:
if not planned_playlist:
fallback_type = str(EngineUtil.get_playlist_type(resolved_playlist.fallback_type))
s += "\n"
s += SimpleUtil.red("No Playlist assigned to schedule. Instead playing the `%s` playlist below ↓↓↓" % SimpleUtil.cyan(fallback_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)
if resolved_playlist:
fallback_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(fallback_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"
requests==2.24.0
sqlalchemy==1.3.17
Flask==1.1.2
Flask_SQLAlchemy==2.4.3
mysqlclient==1.3.12
redis==3.5.3
validators==0.12.1
accessify==0.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:
#
# - init
# - engine
# - core
# - lqs
# - recreate-database
# - docker:engine
# - docker:build
# - docker:push
#
if [[ $* =~ ^(init|env|engine|core|lqs)$ ]]; then
mode=$1
fi
if [[ "$1" == *"docker:"* ]]; then
docker="true"
mode=${1#*:}
fi
echo "[ Run mode=$mode ]"
echo "[ Docker=$docker ]"
# Find the correct Python version (3.7 or 3.8)
if hash python3.8 2>/dev/null; then
PYTHON_EXEC="python3.8"
echo "[ Using Python 3.8 ]"
else
PYTHON_EXEC="python3.7"
echo "[ Using Python 3.7 ]"
fi
# +++ DEFAULT COMMANDS +++ #
if [[ $docker == "false" ]]; then
### Initializes the environment & installs dependencies ###
if [[ $mode == "init" ]]; then
mkdir -p logs
pip3 install -r requirements.txt
fi
### Runs Engine Core & Liquidsoap ###
if [[ $mode == "engine" ]]; then
eval $(opam env)
/usr/bin/env $PYTHON_EXEC engine-core.py
fi
### Runs Engine Core only ###
if [[ $mode == "core" ]]; then
/usr/bin/env $PYTHON_EXEC engine-core.py --without-lqs
fi
### Runs Liquidsoap only ###
if [[ $mode == "lqs" ]]; then
lqs=$(/usr/bin/env $PYTHON_EXEC engine-core.py --get-lqs-command)
eval "$lqs"
fi
### CAUTION: This deletes everything in your database ###
if [[ $mode == "recreate-database" ]]; then
/usr/bin/env $PYTHON_EXEC 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 \
--network="host" \
--name aura-engine \
--rm -d \
-u $UID:$GID \
-v "$BASE_D":/srv \
-v "$BASE_D/audio/source":/var/audio/source:ro \
-v "$BASE_D/configuration/docker":/etc/aura \
-v "/dev/snd":/dev/snd \
--privileged \
--tmpfs /var/log/aura/ \
autoradio/engine
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
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
# 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 Engine
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 = Engine(self.config)
lsc.mixer_status()
return True
except Exception as e:
return False
# ------------------------------------------------------------------------------------------ #
def test_lqsr_conn(self):
try:
lsc = Engine(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)
#!/bin/sh
''''which python3.8 >/dev/null 2>&1 && exec python3.8 "$0" "$@" # '''
''''which python3.7 >/dev/null 2>&1 && exec python3.7 "$0" "$@" # '''
''''exec echo "Error: Snaaakey Python, where are you?" # '''
#
# 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 json
import logging
import sqlalchemy
import decimal
from modules.core.engine import Engine
from modules.base.config import AuraConfig
from modules.base.models 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 select_current_programme():
# select_programme()
config = AuraConfig()
config.read_config()
engine = Engine(config.config)
sched = AuraScheduler(config.config)
engine.scheduler = sched
sched.engine = engine
programme = sched.load_programme_from_db()
for show in programme:
print(show)
def fadeout(lsc):
entry = ScheduleEntry.select_programme()
lsc.fade_out(entry, 2)
def fadein(lsc):
entry = ScheduleEntry.select_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()