Commit ce532118 authored by David Trattnig's avatar David Trattnig
Browse files

Extracted ASCII rendering. #41

parent ca7ee1bd
......@@ -23,7 +23,6 @@ import threading
import time
import sqlalchemy
from enum import Enum
from operator import attrgetter
from datetime import datetime, timedelta
......@@ -36,17 +35,11 @@ from src.core.engine import Engine
from src.core.channels import ChannelType, TransitionType, EntryPlayState
from src.core.resources import ResourceClass, ResourceUtil
from src.scheduling.calendar import AuraCalendarService
from src.scheduling.utils import TimeslotRenderer
class EntryQueueState(Enum):
"""
Types of playlist entry behaviours.
"""
OKAY = "ok"
CUT = "cut"
OUT_OF_SCHEDULE = "oos"
......@@ -104,6 +97,7 @@ class AuraScheduler(threading.Thread):
is_initialized = None
last_successful_fetch = None
timeslot_renderer = None
programme = None
message_timer = []
fallback = None
......@@ -122,7 +116,7 @@ class AuraScheduler(threading.Thread):
"""
self.config = AuraConfig.config()
self.logger = logging.getLogger("AuraEngine")
self.timeslot_renderer = TimeslotRenderer(self)
AuraScheduler.init_database()
self.fallback = fallback_manager
self.engine = engine
......@@ -194,7 +188,7 @@ class AuraScheduler(threading.Thread):
"""
Called when the scheduler is ready.
"""
self.logger.info(self.get_ascii_programme())
self.logger.info(self.timeslot_renderer.get_ascii_timeslots())
try:
self.play_active_entry()
......@@ -347,6 +341,7 @@ class AuraScheduler(threading.Thread):
Args:
max_count (Integer): Maximum of timeslots to return, if `0` all exitsing ones are returned
Returns:
([Timeslot]): The next timeslots
"""
......@@ -406,124 +401,6 @@ class AuraScheduler(threading.Thread):
def get_ascii_programme(self):
"""
Creates a printable version of the current programme (playlists and entries as per timeslot)
Returns:
(String): An ASCII representation of the programme
"""
active_timeslot = self.get_active_timeslot()
s = "\n\n SCHEDULED NOW:"
s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
if active_timeslot:
planned_playlist = None
if active_timeslot.playlist:
planned_playlist = active_timeslot.playlist
(fallback_type, resolved_playlist) = self.fallback.resolve_playlist(active_timeslot)
s += "\n│ Playing timeslot %s " % active_timeslot
if planned_playlist:
if resolved_playlist and resolved_playlist.playlist_id != planned_playlist.playlist_id:
s += "\n│ └── Playlist %s " % planned_playlist
s += "\n│ "
s += SU.red("↑↑↑ That's the originally planned playlist.") + ("Instead playing the fallback playlist below ↓↓↓")
if resolved_playlist:
if not planned_playlist:
s += "\n│ "
s += SU.red("No playlist assigned to timeslot. Instead playing the `%s` playlist below ↓↓↓" % SU.cyan(str(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(active_entry.entry_num+1), SU.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" % (SU.red("No active playlist. There should be at least some fallback playlist running..."))
else:
s += "\n│ Nothing. "
s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────"
s += "\n SCHEDULED NEXT:"
s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
next_timeslots = self.get_next_timeslots()
if not next_timeslots:
s += "\n│ Nothing. "
else:
for timeslot in next_timeslots:
(fallback_type, resolved_playlist) = self.fallback.resolve_playlist(timeslot)
if resolved_playlist:
s += "\n│ Queued timeslot %s " % timeslot
s += "\n│ └── Playlist %s (Type: %s)" % (resolved_playlist, SU.cyan(str(fallback_type)))
if resolved_playlist.end_unix > timeslot.end_unix:
s += "\n│ %s! " % \
(SU.red("↑↑↑ Playlist #%s ends after timeslot #%s!" % (resolved_playlist.playlist_id, timeslot.timeslot_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_timeslot = False
for entry in entries:
if entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE and not is_out_of_timeslot:
s += "\n│ %s" % \
SU.red("↓↓↓ These entries won't be played because they are out of timeslot.")
is_out_of_timeslot = True
s += self.build_entry_string("\n│ └── ", entry, is_out_of_timeslot)
return s
def build_entry_string(self, prefix, entry, strike):
"""
Returns an stringified entry.
"""
s = ""
if entry.queue_state == EntryQueueState.CUT:
s = "\n│ %s" % SU.red("↓↓↓ This entry is going to be cut.")
if strike:
entry_str = SU.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
......@@ -663,10 +540,10 @@ class AuraScheduler(threading.Thread):
index = 0
# Mark entries which start after the end of their timeslot or are cut
clean_entries = self.preprocess_entries(entries, True)
# clean_entries = self.preprocess_entries(entries, True)
# Group/aggregate all filesystem entries, allowing them to be queued at once
for entry in clean_entries:
for entry in entries:
if previous_entry == None or \
(previous_entry != None and \
previous_entry.get_content_type() == entry.get_content_type() and \
......@@ -682,7 +559,7 @@ class AuraScheduler(threading.Thread):
# Timeslot function calls
do_queue_timeslot_end = False
if len(clean_entries) > 0 and len(entry_groups) > 0:
if len(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))
......@@ -691,7 +568,7 @@ class AuraScheduler(threading.Thread):
self.set_entries_timer(entries, fade_in, fade_out)
# Store them for later reference
timeslot.queued_entries = clean_entries
timeslot.queued_entries = entries
else:
self.logger.warn(SU.red("Nothing to schedule ..."))
......@@ -722,7 +599,7 @@ class AuraScheduler(threading.Thread):
# Let 'em play anyway ...
self.engine.player.play(entries[0], transition_type)
self.logger.info(self.get_ascii_programme())
self.logger.info(self.timeslot_renderer.get_ascii_timeslots())
if play_timer:
......@@ -767,34 +644,7 @@ class AuraScheduler(threading.Thread):
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 timeslot' are not returned
Returns:
([PlaylistEntry]): The list of processed playlist entries
"""
clean_entries = []
for entry in entries:
if entry.entry_start >= entry.playlist.timeslot.timeslot_end:
msg = "Filtered entry (%s) after end-of timeslot (%s) ... SKIPPED" % (entry, entry.playlist.timeslot)
self.logger.warning(SU.red(msg))
entry.queue_state = EntryQueueState.OUT_OF_SCHEDULE
elif entry.end_unix > entry.playlist.timeslot.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
......
#
# 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
from enum import Enum
from src.base.utils import SimpleUtil as SU
class EntryQueueState(Enum):
"""
Types of playlist entry behaviours.
"""
OKAY = "ok"
CUT = "cut"
OUT_OF_SCHEDULE = "oos"
class TimeslotRenderer:
"""
Displays current and next timeslots in ASCII for maintainence and debugging.
"""
logger = None
scheduler = None
def __init__(self, scheduler):
"""
Constructor
"""
self.logger = logging.getLogger("AuraEngine")
self.scheduler = scheduler
def get_ascii_timeslots(self):
"""
Creates a printable version of the current programme (playlists and entries as per timeslot)
Returns:
(String): An ASCII representation of the current and next timeslots
"""
active_timeslot = self.scheduler.get_active_timeslot()
s = "\n\n SCHEDULED NOW:"
s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
if active_timeslot:
planned_playlist = None
if active_timeslot.playlist:
planned_playlist = active_timeslot.playlist
(fallback_type, resolved_playlist) = self.scheduler.fallback.resolve_playlist(active_timeslot)
s += "\n│ Playing timeslot %s " % active_timeslot
if planned_playlist:
if resolved_playlist and resolved_playlist.playlist_id != planned_playlist.playlist_id:
s += "\n│ └── Playlist %s " % planned_playlist
s += "\n│ "
s += SU.red("↑↑↑ That's the originally planned playlist.") + ("Instead playing the fallback playlist below ↓↓↓")
if resolved_playlist:
if not planned_playlist:
s += "\n│ "
s += SU.red("No playlist assigned to timeslot. Instead playing the `%s` playlist below ↓↓↓" % SU.cyan(str(fallback_type)))
s += "\n│ └── Playlist %s " % resolved_playlist
active_entry = self.scheduler.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(active_entry.entry_num+1), SU.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" % (SU.red("No active playlist. There should be at least some fallback playlist running..."))
else:
s += "\n│ Nothing. "
s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────"
s += "\n SCHEDULED NEXT:"
s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
next_timeslots = self.scheduler.get_next_timeslots()
if not next_timeslots:
s += "\n│ Nothing. "
else:
for timeslot in next_timeslots:
(fallback_type, resolved_playlist) = self.scheduler.fallback.resolve_playlist(timeslot)
if resolved_playlist:
s += "\n│ Queued timeslot %s " % timeslot
s += "\n│ └── Playlist %s (Type: %s)" % (resolved_playlist, SU.cyan(str(fallback_type)))
if resolved_playlist.end_unix > timeslot.end_unix:
s += "\n│ %s! " % \
(SU.red("↑↑↑ Playlist #%s ends after timeslot #%s!" % (resolved_playlist.playlist_id, timeslot.timeslot_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_timeslot = False
for entry in entries:
if entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE and not is_out_of_timeslot:
s += "\n│ %s" % \
SU.red("↓↓↓ These entries won't be played because they are out of timeslot.")
is_out_of_timeslot = True
s += self.build_entry_string("\n│ └── ", entry, is_out_of_timeslot)
return s
def build_entry_string(self, prefix, entry, strike):
"""
Returns an stringified entry.
"""
s = ""
if entry.queue_state == EntryQueueState.CUT:
s = "\n│ %s" % SU.red("↓↓↓ This entry is going to be cut.")
if strike:
entry_str = SU.strike(entry)
else:
entry_str = str(entry)
entry_line = "%sEntry %s | %s" % (prefix, str(entry.entry_num+1), entry_str)
return s + entry_line
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 timeslot' are not returned
Returns:
([PlaylistEntry]): The list of processed playlist entries
"""
clean_entries = []
for entry in entries:
if entry.entry_start >= entry.playlist.timeslot.timeslot_end:
msg = "Filtered entry (%s) after end-of timeslot (%s) ... SKIPPED" % (entry, entry.playlist.timeslot)
self.logger.warning(SU.red(msg))
entry.queue_state = EntryQueueState.OUT_OF_SCHEDULE
elif entry.end_unix > entry.playlist.timeslot.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
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment