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