From ce5321186cca751a6c6e0eb4dfaf718dd8aa1839 Mon Sep 17 00:00:00 2001 From: David Trattnig <david.trattnig@o94.at> Date: Fri, 13 Nov 2020 12:37:21 +0100 Subject: [PATCH] Extracted ASCII rendering. #41 --- src/scheduling/scheduler.py | 170 ++---------------------------- src/scheduling/utils.py | 203 ++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 160 deletions(-) create mode 100644 src/scheduling/utils.py diff --git a/src/scheduling/scheduler.py b/src/scheduling/scheduler.py index ebabe854..c969d2ec 100644 --- a/src/scheduling/scheduler.py +++ b/src/scheduling/scheduler.py @@ -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 diff --git a/src/scheduling/utils.py b/src/scheduling/utils.py new file mode 100644 index 00000000..ad88e62a --- /dev/null +++ b/src/scheduling/utils.py @@ -0,0 +1,203 @@ + +# +# 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 -- GitLab