Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
aura-engine
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package registry
Container Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Lars Kruse
aura-engine
Commits
f90f2f10
Commit
f90f2f10
authored
5 years ago
by
David Trattnig
Browse files
Options
Downloads
Patches
Plain Diff
Fallback handling.
parent
de452cc0
No related branches found
No related tags found
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
modules/scheduling/fallback_manager.py
+229
-0
229 additions, 0 deletions
modules/scheduling/fallback_manager.py
modules/scheduling/scheduler.py
+20
-57
20 additions, 57 deletions
modules/scheduling/scheduler.py
with
249 additions
and
57 deletions
modules/scheduling/fallback_manager.py
0 → 100644
+
229
−
0
View file @
f90f2f10
#
# Aura Engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2020 David Trattnig <david.trattnig@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/>.
#
# Meta
__version__
=
'
0.0.1
'
__license__
=
"
GNU General Public License (GPL) Version 3
"
__version_info__
=
(
0
,
0
,
1
)
__author__
=
'
David Trattnig <david.trattnig@subsquare.at>
'
import
os
,
os
.
path
import
logging
import
random
from
accessify
import
private
,
protected
from
modules.base.simpleutil
import
SimpleUtil
from
modules.communication.mail
import
AuraMailer
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
.
mailer
=
AuraMailer
(
self
.
config
)
self
.
scheduler
=
scheduler
self
.
logger
=
logger
#
# PUBLIC METHODS
#
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
=
"
../../testing/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!
"
)
# Send admin email to notify about the fallback state
if
file
:
if
not
active_playlist
:
active_playlist
=
"
n/a
"
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
.
critical
(
"
Sent admin email:
\n
──────────────────────────────────────
\n
"
+
msg
)
self
.
is_processing
=
False
return
file
#
# PRIVATE METHODS
#
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
.
filename
+
"
\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
(
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
.
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
).
difference
(
history
))
# 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
,
dir
,
file
):
"""
Checks if the passed file is an audio file i.e. has a file-extension
known for audio files.
Args:
(File): file: the file object.
Returns:
(Boolean): True, if it
'
s an audio file.
"""
audio_extensions
=
[
"
.wav
"
,
"
.flac
"
,
"
.mp3
"
,
"
.ogg
"
]
ext
=
os
.
path
.
splitext
(
file
)[
1
]
abs_path
=
os
.
path
.
join
(
dir
,
file
)
if
os
.
path
.
isfile
(
abs_path
):
if
any
(
ext
in
s
for
s
in
audio_extensions
):
return
True
return
False
\ No newline at end of file
This diff is collapsed.
Click to expand it.
modules/scheduling/scheduler.py
+
20
−
57
View file @
f90f2f10
...
...
@@ -35,7 +35,6 @@ import datetime
import
decimal
import
traceback
import
sqlalchemy
import
logging
import
threading
...
...
@@ -44,6 +43,7 @@ from operator import attrgetter
from
modules.base.simpleutil
import
SimpleUtil
from
modules.communication.redis.messenger
import
RedisMessenger
from
modules.scheduling.calendar
import
AuraCalendarService
from
modules.scheduling.fallback_manager
import
FallbackManager
from
libraries.database.broadcasts
import
Schedule
,
Playlist
,
AuraDatabaseModel
from
libraries.exceptions.exception_logger
import
ExceptionLogger
from
libraries.enum.auraenumerations
import
ScheduleEntryType
,
TimerType
,
TerminalColors
...
...
@@ -90,11 +90,10 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
exit_event
=
None
liquidsoapcommunicator
=
None
last_successful_fetch
=
None
programme
=
None
active_entry
=
None
message_timer
=
[]
fallback_manager
=
None
#schedule_entries = None
client
=
None
...
...
@@ -110,7 +109,7 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
self
.
logger
=
logging
.
getLogger
(
"
AuraEngine
"
)
self
.
init_error_messages
()
self
.
init_database
()
self
.
fallback_manager
=
FallbackManager
(
config
,
self
.
logger
,
self
)
self
.
redismessenger
=
RedisMessenger
(
config
)
# init threading
...
...
@@ -276,35 +275,20 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
def
get_next_file_for
(
self
,
fallbackname
):
"""
Evaluates the next **fallback files/
folder
s** to be played for a given fallback-type.
Evaluates the next **fallback files/
playlist
s** 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
=
None
# next_entry = None
# if not self.active_entry:
# self.get_active_entry()
# next_entry = self.active_entry
# else:
# next_entry = self.get_next_entry()
if
fallbackname
==
"
timeslot
"
:
file
=
"
/home/david/Music/ab.mp3
"
elif
fallbackname
==
"
show
"
:
file
=
"
/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3
"
elif
fallbackname
==
"
station
"
:
file
=
"
/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3
"
else
:
file
=
""
self
.
logger
.
critical
(
"
Should set next fallback file for
"
+
fallbackname
+
"
, but this fallback is unknown!
"
)
file
=
self
.
fallback_manager
.
get_fallback_for
(
fallbackname
)
if
file
:
self
.
logger
.
info
(
"
Got next file
'
%s
'
(type: %s)
"
%
(
file
,
fallbackname
))
...
...
@@ -336,14 +320,15 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
current_playlist
=
None
# Iterate over all shows and playlists and find the one to be played right now
for
schedule
in
self
.
programme
:
if
schedule
.
start_unix
<
now_unix
<
schedule
.
end_unix
:
current_schedule
=
schedule
for
playlist
in
schedule
.
playlist
:
if
playlist
.
start_unix
<
now_unix
<
playlist
.
end_unix
:
current_playlist
=
playlist
break
break
if
self
.
programme
:
for
schedule
in
self
.
programme
:
if
schedule
.
start_unix
<
now_unix
<
schedule
.
end_unix
:
current_schedule
=
schedule
for
playlist
in
schedule
.
playlist
:
if
playlist
.
start_unix
<
now_unix
<
playlist
.
end_unix
:
current_playlist
=
playlist
break
break
return
(
current_schedule
,
current_playlist
)
...
...
@@ -523,9 +508,9 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
# Always load latest programme from the database
self
.
last_successful_fetch
=
lsf
self
.
load_programme_from_db
()
self
.
logger
.
info
(
"
Finished loading current programme from database
"
)
self
.
logger
.
info
(
"
Finished loading current programme from database
(%s schedules)
"
%
str
(
len
(
self
.
programme
))
)
for
schedule
in
self
.
programme
:
self
.
logger
.
debug
(
"
\t
Schedule %s with Playlist %s
"
%
(
str
(
schedule
),
str
(
schedule
.
playlist
[
0
]
)))
self
.
logger
.
debug
(
"
\t
Schedule %s with Playlist %s
"
%
(
str
(
schedule
),
str
(
schedule
.
playlist
)))
...
...
@@ -542,31 +527,9 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
self
.
logger
.
critical
(
"
Could not load programme from database. We are in big trouble my friend!
"
)
return
# FIXME That's very likely not needed - review!
# planned_entries = []
# for schedule in self.programme:
# # playlist to play
# #schedule.playlist = [Playlist.select_playlist_for_schedule(schedule.schedule_start, schedule.playlist_id)]
# # show fallback is played when playlist fails
# #schedule.showfallback = Playlist.select_playlist(schedule.show_fallback_id)
# # timeslot fallback is played when show fallback fails
# #schedule.timeslotfallback = Playlist.select_playlist(schedule.timeslot_fallback_id)
# # station fallback is played when timeslot fallback fails
# #schedule.stationfallback = Playlist.select_playlist(schedule.station_fallback_id)
# for p in schedule.playlist:
# planned_entries.append(p)
# FIXME Same playlists are repeated over time - test with different schedules/timeslots/playlists
# Therefore only passing the first playlist for now:
# self.logger.warn("ONLY PASSING 1ST PLAYLIST OF PROGRAMME")
# self.enable_entries(planned_entries[0])
# FIXME Still needed?
def
enable_entries
(
self
,
playlist
):
"""
Iterates over all playlist entries and assigs their start time.
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment