forked from rc/aircox
code quality
This commit is contained in:
@ -1,11 +1,13 @@
|
||||
"""
|
||||
Handle the audio streamer and controls it as we want it to be. It is
|
||||
used to:
|
||||
"""Handle the audio streamer and controls it as we want it to be. It is used
|
||||
to:
|
||||
|
||||
- generate config files and playlists;
|
||||
- monitor Liquidsoap, logs and scheduled programs;
|
||||
- cancels Diffusions that have an archive but could not have been played;
|
||||
- run Liquidsoap
|
||||
"""
|
||||
import time
|
||||
|
||||
# TODO:
|
||||
# x controllers: remaining
|
||||
# x diffusion conflicts
|
||||
@ -14,17 +16,12 @@ used to:
|
||||
# - handle restart after failure
|
||||
# - is stream restart after live ok?
|
||||
from argparse import RawTextHelpFormatter
|
||||
import time
|
||||
|
||||
import pytz
|
||||
from django.db.models import Q
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
|
||||
from aircox.utils import date_range
|
||||
|
||||
|
||||
from aircox.models import Diffusion, Log, Sound, Station, Track
|
||||
from aircox_streamer.controllers import Streamer
|
||||
|
||||
# force using UTC
|
||||
@ -32,8 +29,7 @@ tz.activate(pytz.UTC)
|
||||
|
||||
|
||||
class Monitor:
|
||||
"""
|
||||
Log and launch diffusions for the given station.
|
||||
"""Log and launch diffusions for the given station.
|
||||
|
||||
Monitor should be able to be used after a crash a go back
|
||||
where it was playing, so we heavily use logs to be able to
|
||||
@ -44,20 +40,21 @@ class Monitor:
|
||||
- scheduled diffusions
|
||||
- tracks for sounds of streamed programs
|
||||
"""
|
||||
|
||||
streamer = None
|
||||
""" Streamer controller """
|
||||
"""Streamer controller."""
|
||||
delay = None
|
||||
""" Timedelta: minimal delay between two call of monitor. """
|
||||
logs = None
|
||||
""" Queryset to station's logs (ordered by -pk) """
|
||||
"""Queryset to station's logs (ordered by -pk)"""
|
||||
cancel_timeout = 20
|
||||
""" Timeout in minutes before cancelling a diffusion. """
|
||||
"""Timeout in minutes before cancelling a diffusion."""
|
||||
sync_timeout = 5
|
||||
""" Timeout in minutes between two streamer's sync. """
|
||||
"""Timeout in minutes between two streamer's sync."""
|
||||
sync_next = None
|
||||
""" Datetime of the next sync. """
|
||||
"""Datetime of the next sync."""
|
||||
last_sound_logs = None
|
||||
""" Last logged sounds, as ``{source_id: log}``. """
|
||||
"""Last logged sounds, as ``{source_id: log}``."""
|
||||
|
||||
@property
|
||||
def station(self):
|
||||
@ -65,12 +62,12 @@ class Monitor:
|
||||
|
||||
@property
|
||||
def last_log(self):
|
||||
""" Last log of monitored station. """
|
||||
"""Last log of monitored station."""
|
||||
return self.logs.first()
|
||||
|
||||
@property
|
||||
def last_diff_start(self):
|
||||
""" Log of last triggered item (sound or diffusion). """
|
||||
"""Log of last triggered item (sound or diffusion)."""
|
||||
return self.logs.start().with_diff().first()
|
||||
|
||||
def __init__(self, streamer, delay, cancel_timeout, **kwargs):
|
||||
@ -83,12 +80,13 @@ class Monitor:
|
||||
self.init_last_sound_logs()
|
||||
|
||||
def get_logs_queryset(self):
|
||||
""" Return queryset to assign as `self.logs` """
|
||||
return self.station.log_set.select_related('diffusion', 'sound', 'track') \
|
||||
.order_by('-pk')
|
||||
"""Return queryset to assign as `self.logs`"""
|
||||
return self.station.log_set.select_related(
|
||||
"diffusion", "sound", "track"
|
||||
).order_by("-pk")
|
||||
|
||||
def init_last_sound_logs(self, key=None):
|
||||
""" Retrieve last logs and initialize `last_sound_logs` """
|
||||
"""Retrieve last logs and initialize `last_sound_logs`"""
|
||||
logs = {}
|
||||
for source in self.streamer.sources:
|
||||
qs = self.logs.filter(source=source.id, sound__isnull=False)
|
||||
@ -96,7 +94,7 @@ class Monitor:
|
||||
self.last_sound_logs = logs
|
||||
|
||||
def monitor(self):
|
||||
""" Run all monitoring functions once. """
|
||||
"""Run all monitoring functions once."""
|
||||
if not self.streamer.is_ready:
|
||||
return
|
||||
|
||||
@ -128,15 +126,15 @@ class Monitor:
|
||||
if log:
|
||||
self.trace_tracks(log)
|
||||
else:
|
||||
print('no source or sound for stream; source = ', source)
|
||||
print("no source or sound for stream; source = ", source)
|
||||
|
||||
self.handle_diffusions()
|
||||
self.sync()
|
||||
|
||||
def log(self, source, **kwargs):
|
||||
""" Create a log using **kwargs, and print info """
|
||||
kwargs.setdefault('station', self.station)
|
||||
kwargs.setdefault('date', tz.now())
|
||||
"""Create a log using **kwargs, and print info."""
|
||||
kwargs.setdefault("station", self.station)
|
||||
kwargs.setdefault("date", tz.now())
|
||||
log = Log(source=source, **kwargs)
|
||||
log.save()
|
||||
log.print()
|
||||
@ -146,7 +144,7 @@ class Monitor:
|
||||
return log
|
||||
|
||||
def trace_sound(self, source):
|
||||
""" Return on air sound log (create if not present). """
|
||||
"""Return on air sound log (create if not present)."""
|
||||
air_uri, air_time = source.uri, source.air_time
|
||||
last_log = self.last_sound_logs.get(source.id)
|
||||
if last_log and last_log.sound.file.path == source.uri:
|
||||
@ -169,24 +167,31 @@ class Monitor:
|
||||
diff = None
|
||||
sound = Sound.objects.path(air_uri).first()
|
||||
if sound and sound.episode_id is not None:
|
||||
diff = Diffusion.objects.episode(id=sound.episode_id).on_air() \
|
||||
.now(air_time).first()
|
||||
diff = (
|
||||
Diffusion.objects.episode(id=sound.episode_id)
|
||||
.on_air()
|
||||
.now(air_time)
|
||||
.first()
|
||||
)
|
||||
|
||||
# log sound on air
|
||||
return self.log(type=Log.TYPE_ON_AIR, date=source.air_time,
|
||||
source=source.id, sound=sound, diffusion=diff,
|
||||
comment=air_uri)
|
||||
return self.log(
|
||||
type=Log.TYPE_ON_AIR,
|
||||
date=source.air_time,
|
||||
source=source.id,
|
||||
sound=sound,
|
||||
diffusion=diff,
|
||||
comment=air_uri,
|
||||
)
|
||||
|
||||
def trace_tracks(self, log):
|
||||
"""
|
||||
Log tracks for the given sound log (for streamed programs only).
|
||||
"""
|
||||
"""Log tracks for the given sound log (for streamed programs only)."""
|
||||
if log.diffusion:
|
||||
return
|
||||
|
||||
tracks = Track.objects \
|
||||
.filter(sound__id=log.sound_id, timestamp__isnull=False)\
|
||||
.order_by('timestamp')
|
||||
tracks = Track.objects.filter(
|
||||
sound__id=log.sound_id, timestamp__isnull=False
|
||||
).order_by("timestamp")
|
||||
if not tracks.exists():
|
||||
return
|
||||
|
||||
@ -197,14 +202,17 @@ class Monitor:
|
||||
pos = log.date + tz.timedelta(seconds=track.timestamp)
|
||||
if pos > now:
|
||||
break
|
||||
self.log(type=Log.TYPE_ON_AIR, date=pos, source=log.source,
|
||||
track=track, comment=track)
|
||||
self.log(
|
||||
type=Log.TYPE_ON_AIR,
|
||||
date=pos,
|
||||
source=log.source,
|
||||
track=track,
|
||||
comment=track,
|
||||
)
|
||||
|
||||
def handle_diffusions(self):
|
||||
"""
|
||||
Handle scheduled diffusion, trigger if needed, preload playlists
|
||||
and so on.
|
||||
"""
|
||||
"""Handle scheduled diffusion, trigger if needed, preload playlists and
|
||||
so on."""
|
||||
# TODO: program restart
|
||||
|
||||
# Diffusion conflicts are handled by the way a diffusion is defined
|
||||
@ -227,9 +235,13 @@ class Monitor:
|
||||
# ```
|
||||
#
|
||||
now = tz.now()
|
||||
diff = Diffusion.objects.station(self.station).on_air().now(now) \
|
||||
.filter(episode__sound__type=Sound.TYPE_ARCHIVE) \
|
||||
.first()
|
||||
diff = (
|
||||
Diffusion.objects.station(self.station)
|
||||
.on_air()
|
||||
.now(now)
|
||||
.filter(episode__sound__type=Sound.TYPE_ARCHIVE)
|
||||
.first()
|
||||
)
|
||||
# Can't use delay: diffusion may start later than its assigned start.
|
||||
log = None if not diff else self.logs.start().filter(diffusion=diff)
|
||||
if not diff or log:
|
||||
@ -237,8 +249,11 @@ class Monitor:
|
||||
|
||||
dealer = self.streamer.dealer
|
||||
# start
|
||||
if not dealer.queue and dealer.rid is None or \
|
||||
dealer.remaining < self.delay.total_seconds():
|
||||
if (
|
||||
not dealer.queue
|
||||
and dealer.rid is None
|
||||
or dealer.remaining < self.delay.total_seconds()
|
||||
):
|
||||
self.start_diff(dealer, diff)
|
||||
|
||||
# cancel
|
||||
@ -248,17 +263,25 @@ class Monitor:
|
||||
def start_diff(self, source, diff):
|
||||
playlist = Sound.objects.episode(id=diff.episode_id).playlist()
|
||||
source.push(*playlist)
|
||||
self.log(type=Log.TYPE_START, source=source.id, diffusion=diff,
|
||||
comment=str(diff))
|
||||
self.log(
|
||||
type=Log.TYPE_START,
|
||||
source=source.id,
|
||||
diffusion=diff,
|
||||
comment=str(diff),
|
||||
)
|
||||
|
||||
def cancel_diff(self, source, diff):
|
||||
diff.type = Diffusion.TYPE_CANCEL
|
||||
diff.save()
|
||||
self.log(type=Log.TYPE_CANCEL, source=source.id, diffusion=diff,
|
||||
comment=str(diff))
|
||||
self.log(
|
||||
type=Log.TYPE_CANCEL,
|
||||
source=source.id,
|
||||
diffusion=diff,
|
||||
comment=str(diff),
|
||||
)
|
||||
|
||||
def sync(self):
|
||||
""" Update sources' playlists. """
|
||||
"""Update sources' playlists."""
|
||||
now = tz.now()
|
||||
if self.sync_next is not None and now < self.sync_next:
|
||||
return
|
||||
@ -269,55 +292,82 @@ class Monitor:
|
||||
source.sync()
|
||||
|
||||
|
||||
class Command (BaseCommand):
|
||||
class Command(BaseCommand):
|
||||
help = __doc__
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.formatter_class = RawTextHelpFormatter
|
||||
group = parser.add_argument_group('actions')
|
||||
group = parser.add_argument_group("actions")
|
||||
group.add_argument(
|
||||
'-c', '--config', action='store_true',
|
||||
help='generate configuration files for the stations'
|
||||
"-c",
|
||||
"--config",
|
||||
action="store_true",
|
||||
help="generate configuration files for the stations",
|
||||
)
|
||||
group.add_argument(
|
||||
'-m', '--monitor', action='store_true',
|
||||
help='monitor the scheduled diffusions and log what happens'
|
||||
"-m",
|
||||
"--monitor",
|
||||
action="store_true",
|
||||
help="monitor the scheduled diffusions and log what happens",
|
||||
)
|
||||
group.add_argument(
|
||||
'-r', '--run', action='store_true',
|
||||
help='run the required applications for the stations'
|
||||
"-r",
|
||||
"--run",
|
||||
action="store_true",
|
||||
help="run the required applications for the stations",
|
||||
)
|
||||
|
||||
group = parser.add_argument_group('options')
|
||||
group = parser.add_argument_group("options")
|
||||
group.add_argument(
|
||||
'-d', '--delay', type=int,
|
||||
"-d",
|
||||
"--delay",
|
||||
type=int,
|
||||
default=1000,
|
||||
help='time to sleep in MILLISECONDS between two updates when we '
|
||||
'monitor. This influence the delay before a diffusion is '
|
||||
'launched.'
|
||||
help="time to sleep in MILLISECONDS between two updates when we "
|
||||
"monitor. This influence the delay before a diffusion is "
|
||||
"launched.",
|
||||
)
|
||||
group.add_argument(
|
||||
'-s', '--station', type=str, action='append',
|
||||
help='name of the station to monitor instead of monitoring '
|
||||
'all stations'
|
||||
"-s",
|
||||
"--station",
|
||||
type=str,
|
||||
action="append",
|
||||
help="name of the station to monitor instead of monitoring "
|
||||
"all stations",
|
||||
)
|
||||
group.add_argument(
|
||||
'-t', '--timeout', type=float,
|
||||
"-t",
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=Monitor.cancel_timeout,
|
||||
help='time to wait in MINUTES before canceling a diffusion that '
|
||||
'should have ran but did not. '
|
||||
help="time to wait in MINUTES before canceling a diffusion that "
|
||||
"should have ran but did not. ",
|
||||
)
|
||||
# TODO: sync-timeout, cancel-timeout
|
||||
|
||||
def handle(self, *args, config=None, run=None, monitor=None, station=[],
|
||||
delay=1000, timeout=600, **options):
|
||||
stations = Station.objects.filter(name__in=station) if station else \
|
||||
Station.objects.all()
|
||||
def handle(
|
||||
self,
|
||||
*args,
|
||||
config=None,
|
||||
run=None,
|
||||
monitor=None,
|
||||
station=[],
|
||||
delay=1000,
|
||||
timeout=600,
|
||||
**options
|
||||
):
|
||||
stations = (
|
||||
Station.objects.filter(name__in=station)
|
||||
if station
|
||||
else Station.objects.all()
|
||||
)
|
||||
streamers = [Streamer(station) for station in stations]
|
||||
|
||||
for streamer in streamers:
|
||||
if not streamer.outputs:
|
||||
raise RuntimeError("Streamer {} has no outputs".format(streamer.id))
|
||||
raise RuntimeError(
|
||||
"Streamer {} has no outputs".format(streamer.id)
|
||||
)
|
||||
if config:
|
||||
streamer.make_config()
|
||||
if run:
|
||||
@ -326,8 +376,9 @@ class Command (BaseCommand):
|
||||
if monitor:
|
||||
delay = tz.timedelta(milliseconds=delay)
|
||||
timeout = tz.timedelta(minutes=timeout)
|
||||
monitors = [Monitor(streamer, delay, timeout)
|
||||
for streamer in streamers]
|
||||
monitors = [
|
||||
Monitor(streamer, delay, timeout) for streamer in streamers
|
||||
]
|
||||
|
||||
while not run or streamer.is_running:
|
||||
for monitor in monitors:
|
||||
|
Reference in New Issue
Block a user