forked from rc/aircox
		
	redesign streamer, make it sexy
This commit is contained in:
		@ -1,3 +0,0 @@
 | 
				
			|||||||
include LICENSE
 | 
					 | 
				
			||||||
include README.md
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ -22,7 +22,7 @@ class StationAdmin(admin.ModelAdmin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@admin.register(Log)
 | 
					@admin.register(Log)
 | 
				
			||||||
class LogAdmin(admin.ModelAdmin):
 | 
					class LogAdmin(admin.ModelAdmin):
 | 
				
			||||||
    list_display = ['id', 'date', 'station', 'source', 'type', 'diffusion', 'sound', 'track']
 | 
					    list_display = ['id', 'date', 'station', 'source', 'type', 'comment']
 | 
				
			||||||
    list_filter = ['date', 'source', 'station']
 | 
					    list_filter = ['date', 'source', 'station']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							@ -17,6 +17,10 @@ from .models import Port, Station, Sound
 | 
				
			|||||||
from .connector import Connector
 | 
					from .connector import Connector
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# FIXME liquidsoap does not manage timezones -- we have to convert
 | 
				
			||||||
 | 
					#       'on_air' metadata we get from it into utc one in order to work
 | 
				
			||||||
 | 
					#       correctly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
local_tz = tzlocal.get_localzone()
 | 
					local_tz = tzlocal.get_localzone()
 | 
				
			||||||
logger = logging.getLogger('aircox')
 | 
					logger = logging.getLogger('aircox')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -32,7 +36,11 @@ class Streamer:
 | 
				
			|||||||
    sources = None
 | 
					    sources = None
 | 
				
			||||||
    """ List of all monitored sources """
 | 
					    """ List of all monitored sources """
 | 
				
			||||||
    source = None
 | 
					    source = None
 | 
				
			||||||
    """ Current on air source """
 | 
					    """ Current source being played on air """
 | 
				
			||||||
 | 
					    # note: we disable on_air rids since we don't have use of it for the
 | 
				
			||||||
 | 
					    # moment
 | 
				
			||||||
 | 
					    # on_air = None
 | 
				
			||||||
 | 
					    # """ On-air request ids (rid) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, station):
 | 
					    def __init__(self, station):
 | 
				
			||||||
        self.station = station
 | 
					        self.station = station
 | 
				
			||||||
@ -46,6 +54,27 @@ class Streamer:
 | 
				
			|||||||
        """ Path to Unix socket file """
 | 
					        """ Path to Unix socket file """
 | 
				
			||||||
        return self.connector.address
 | 
					        return self.connector.address
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_ready(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        If external program is ready to use, returns True
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.send('list') != ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_running(self):
 | 
				
			||||||
 | 
					        if self.process is None:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        returncode = self.process.poll()
 | 
				
			||||||
 | 
					        if returncode is None:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.process = None
 | 
				
			||||||
 | 
					        logger.debug('process died with return code %s' % returncode)
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # FIXME: is it really needed as property?
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def inputs(self):
 | 
					    def inputs(self):
 | 
				
			||||||
        """ Return input ports of the station """
 | 
					        """ Return input ports of the station """
 | 
				
			||||||
@ -62,13 +91,6 @@ class Streamer:
 | 
				
			|||||||
            active=True,
 | 
					            active=True,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def is_ready(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        If external program is ready to use, returns True
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return self.send('list') != ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Sources and config ###############################################
 | 
					    # Sources and config ###############################################
 | 
				
			||||||
    def send(self, *args, **kwargs):
 | 
					    def send(self, *args, **kwargs):
 | 
				
			||||||
        return self.connector.send(*args, **kwargs) or ''
 | 
					        return self.connector.send(*args, **kwargs) or ''
 | 
				
			||||||
@ -98,26 +120,27 @@ class Streamer:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def sync(self):
 | 
					    def sync(self):
 | 
				
			||||||
        """ Sync all sources. """
 | 
					        """ Sync all sources. """
 | 
				
			||||||
 | 
					        if self.process is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for source in self.sources:
 | 
					        for source in self.sources:
 | 
				
			||||||
            source.sync()
 | 
					            source.sync()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def fetch(self):
 | 
					    def fetch(self):
 | 
				
			||||||
        """ Fetch data from liquidsoap """
 | 
					        """ Fetch data from liquidsoap """
 | 
				
			||||||
 | 
					        if self.process is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for source in self.sources:
 | 
					        for source in self.sources:
 | 
				
			||||||
            source.fetch()
 | 
					            source.fetch()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        rid = self.send('request.on_air').split(' ')
 | 
					        # request.on_air is not ordered: we need to do it manually
 | 
				
			||||||
        if rid:
 | 
					        if self.dealer.is_playing:
 | 
				
			||||||
            rid = rid[-1]
 | 
					            self.source = self.dealer
 | 
				
			||||||
            # data = self._send('request.metadata ', rid, parse=True)
 | 
					            return
 | 
				
			||||||
            # if not data:
 | 
					 | 
				
			||||||
            #     return
 | 
					 | 
				
			||||||
            pred = lambda s: s.rid == rid
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            pred = lambda s: s.is_playing
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.source = next((source for source in self.sources if pred(source)),
 | 
					        self.source = next((source for source in self.sources
 | 
				
			||||||
                           self.source)
 | 
					                           if source.is_playing), None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Process ##########################################################
 | 
					    # Process ##########################################################
 | 
				
			||||||
    def get_process_args(self):
 | 
					    def get_process_args(self):
 | 
				
			||||||
@ -152,10 +175,8 @@ class Streamer:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def kill_process(self):
 | 
					    def kill_process(self):
 | 
				
			||||||
        if self.process:
 | 
					        if self.process:
 | 
				
			||||||
            logger.info("kill process {pid}: {info}".format(
 | 
					            logger.debug("kill process %s: %s", self.process.pid,
 | 
				
			||||||
                pid=self.process.pid,
 | 
					                         ' '.join(self.get_process_args()))
 | 
				
			||||||
                info=' '.join(self.get_process_args())
 | 
					 | 
				
			||||||
            ))
 | 
					 | 
				
			||||||
            self.process.kill()
 | 
					            self.process.kill()
 | 
				
			||||||
            self.process = None
 | 
					            self.process = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -170,12 +191,19 @@ class Streamer:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class Source:
 | 
					class Source:
 | 
				
			||||||
    controller = None
 | 
					    controller = None
 | 
				
			||||||
 | 
					    """ parent controller """
 | 
				
			||||||
    id = None
 | 
					    id = None
 | 
				
			||||||
 | 
					    """ source id """
 | 
				
			||||||
    uri = ''
 | 
					    uri = ''
 | 
				
			||||||
 | 
					    """ source uri """
 | 
				
			||||||
    rid = None
 | 
					    rid = None
 | 
				
			||||||
 | 
					    """ request id """
 | 
				
			||||||
    air_time = None
 | 
					    air_time = None
 | 
				
			||||||
 | 
					    """ on air time """
 | 
				
			||||||
    status = None
 | 
					    status = None
 | 
				
			||||||
 | 
					    """ source status """
 | 
				
			||||||
 | 
					    remaining = 0.0
 | 
				
			||||||
 | 
					    """ remaining time """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def station(self):
 | 
					    def station(self):
 | 
				
			||||||
@ -185,6 +213,10 @@ class Source:
 | 
				
			|||||||
    def is_playing(self):
 | 
					    def is_playing(self):
 | 
				
			||||||
        return self.status == 'playing'
 | 
					        return self.status == 'playing'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #@property
 | 
				
			||||||
 | 
					    #def is_on_air(self):
 | 
				
			||||||
 | 
					    #    return self.rid is not None and self.rid in self.controller.on_air
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, controller, id=None):
 | 
					    def __init__(self, controller, id=None):
 | 
				
			||||||
        self.controller = controller
 | 
					        self.controller = controller
 | 
				
			||||||
        self.id = id
 | 
					        self.id = id
 | 
				
			||||||
@ -194,14 +226,17 @@ class Source:
 | 
				
			|||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def fetch(self):
 | 
					    def fetch(self):
 | 
				
			||||||
 | 
					        data = self.controller.send(self.id, '.remaining')
 | 
				
			||||||
 | 
					        self.remaining = float(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = self.controller.send(self.id, '.get', parse=True)
 | 
					        data = self.controller.send(self.id, '.get', parse=True)
 | 
				
			||||||
        self.on_metadata(data if data and isinstance(data, dict) else {})
 | 
					        self.on_metadata(data if data and isinstance(data, dict) else {})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def on_metadata(self, data):
 | 
					    def on_metadata(self, data):
 | 
				
			||||||
        """ Update source info from provided request metadata """
 | 
					        """ Update source info from provided request metadata """
 | 
				
			||||||
        self.rid = data.get('rid')
 | 
					        self.rid = data.get('rid') or None
 | 
				
			||||||
        self.uri = data.get('initial_uri')
 | 
					        self.uri = data.get('initial_uri') or None
 | 
				
			||||||
        self.status = data.get('status')
 | 
					        self.status = data.get('status') or None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        air_time = data.get('on_air')
 | 
					        air_time = data.get('on_air')
 | 
				
			||||||
        if air_time:
 | 
					        if air_time:
 | 
				
			||||||
@ -277,8 +312,17 @@ class PlaylistSource(Source):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class QueueSource(Source):
 | 
					class QueueSource(Source):
 | 
				
			||||||
    def queue(self, *paths):
 | 
					    queue = None
 | 
				
			||||||
 | 
					    """ Source's queue (excluded on_air request) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def append(self, *paths):
 | 
				
			||||||
        """ Add the provided paths to source's play queue """
 | 
					        """ Add the provided paths to source's play queue """
 | 
				
			||||||
        for path in paths:
 | 
					        for path in paths:
 | 
				
			||||||
            print(self.controller.send(self.id, '_queue.push ', path))
 | 
					            self.controller.send(self.id, '_queue.push ', path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def fetch(self):
 | 
				
			||||||
 | 
					        super().fetch()
 | 
				
			||||||
 | 
					        queue = self.controller.send(self.id, '_queue.queue').split(' ')
 | 
				
			||||||
 | 
					        self.queue = queue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,7 @@ from django.utils import timezone as tz
 | 
				
			|||||||
import aircox.settings as settings
 | 
					import aircox.settings as settings
 | 
				
			||||||
from aircox.models import Log, Station
 | 
					from aircox.models import Log, Station
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger('aircox.tools')
 | 
					logger = logging.getLogger('aircox.commands')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Command (BaseCommand):
 | 
					class Command (BaseCommand):
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,7 @@ from django.utils import timezone as tz
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from aircox.models import Schedule, Diffusion
 | 
					from aircox.models import Schedule, Diffusion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger('aircox.tools')
 | 
					logger = logging.getLogger('aircox.commands')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Actions:
 | 
					class Actions:
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,7 @@ from aircox.models import *
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
__doc__ = __doc__.format(settings=settings)
 | 
					__doc__ = __doc__.format(settings=settings)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger('aircox.tools')
 | 
					logger = logging.getLogger('aircox.commands')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PlaylistImport:
 | 
					class PlaylistImport:
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,7 @@ from aircox import settings, utils
 | 
				
			|||||||
from aircox.models import Diffusion, Program, Sound
 | 
					from aircox.models import Diffusion, Program, Sound
 | 
				
			||||||
from .import_playlist import PlaylistImport
 | 
					from .import_playlist import PlaylistImport
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger('aircox.tools')
 | 
					logger = logging.getLogger('aircox.commands')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sound_path_re = re.compile(
 | 
					sound_path_re = re.compile(
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ from argparse import RawTextHelpFormatter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.core.management.base import BaseCommand, CommandError
 | 
					from django.core.management.base import BaseCommand, CommandError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger('aircox.tools')
 | 
					logger = logging.getLogger('aircox.commands')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Stats:
 | 
					class Stats:
 | 
				
			||||||
 | 
				
			|||||||
@ -6,27 +6,31 @@ used to:
 | 
				
			|||||||
- cancels Diffusions that have an archive but could not have been played;
 | 
					- cancels Diffusions that have an archive but could not have been played;
 | 
				
			||||||
- run Liquidsoap
 | 
					- run Liquidsoap
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					# TODO:
 | 
				
			||||||
 | 
					# x controllers: remaining
 | 
				
			||||||
 | 
					# x diffusion conflicts
 | 
				
			||||||
 | 
					# x cancel
 | 
				
			||||||
 | 
					# x when liquidsoap fails to start/exists: exit
 | 
				
			||||||
 | 
					# - handle restart after failure
 | 
				
			||||||
 | 
					# - file in queue without sound not logged?
 | 
				
			||||||
 | 
					# - is stream restart after live ok?
 | 
				
			||||||
from argparse import RawTextHelpFormatter
 | 
					from argparse import RawTextHelpFormatter
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytz
 | 
					import pytz
 | 
				
			||||||
import tzlocal
 | 
					from django.db.models import Q
 | 
				
			||||||
from django.core.management.base import BaseCommand
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
from django.utils import timezone as tz
 | 
					from django.utils import timezone as tz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
 | 
					 | 
				
			||||||
from aircox.controllers import Streamer, PlaylistSource
 | 
					from aircox.controllers import Streamer, PlaylistSource
 | 
				
			||||||
 | 
					from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
 | 
				
			||||||
 | 
					from aircox.utils import date_range
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# force using UTC
 | 
					# force using UTC
 | 
				
			||||||
tz.activate(pytz.UTC)
 | 
					tz.activate(pytz.UTC)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# FIXME liquidsoap does not manage timezones -- we have to convert
 | 
					 | 
				
			||||||
#       'on_air' metadata we get from it into utc one in order to work
 | 
					 | 
				
			||||||
#       correctly.
 | 
					 | 
				
			||||||
local_tz = tzlocal.get_localzone()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Monitor:
 | 
					class Monitor:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Log and launch diffusions for the given station.
 | 
					    Log and launch diffusions for the given station.
 | 
				
			||||||
@ -42,6 +46,8 @@ class Monitor:
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    streamer = None
 | 
					    streamer = None
 | 
				
			||||||
    """ Streamer controller """
 | 
					    """ Streamer controller """
 | 
				
			||||||
 | 
					    delay = None
 | 
				
			||||||
 | 
					    """ Timedelta: minimal delay between two call of monitor. """
 | 
				
			||||||
    logs = None
 | 
					    logs = None
 | 
				
			||||||
    """ Queryset to station's logs (ordered by -pk) """
 | 
					    """ Queryset to station's logs (ordered by -pk) """
 | 
				
			||||||
    cancel_timeout = 20
 | 
					    cancel_timeout = 20
 | 
				
			||||||
@ -65,8 +71,11 @@ class Monitor:
 | 
				
			|||||||
        """ Log of last triggered item (sound or diffusion). """
 | 
					        """ Log of last triggered item (sound or diffusion). """
 | 
				
			||||||
        return self.logs.start().with_diff().first()
 | 
					        return self.logs.start().with_diff().first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, streamer, **kwargs):
 | 
					    def __init__(self, streamer, delay, cancel_timeout, **kwargs):
 | 
				
			||||||
        self.streamer = streamer
 | 
					        self.streamer = streamer
 | 
				
			||||||
 | 
					        # adding time ensure all calculation have a margin
 | 
				
			||||||
 | 
					        self.delay = delay + tz.timedelta(seconds=5)
 | 
				
			||||||
 | 
					        self.cancel_timeout = cancel_timeout
 | 
				
			||||||
        self.__dict__.update(kwargs)
 | 
					        self.__dict__.update(kwargs)
 | 
				
			||||||
        self.logs = self.get_logs_queryset()
 | 
					        self.logs = self.get_logs_queryset()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -81,6 +90,27 @@ class Monitor:
 | 
				
			|||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.streamer.fetch()
 | 
					        self.streamer.fetch()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Skip tracing - analyzis:
 | 
				
			||||||
 | 
					        # Reason: multiple database request every x seconds, reducing it.
 | 
				
			||||||
 | 
					        # We could skip this part when remaining time is higher than a minimal
 | 
				
			||||||
 | 
					        # value (which should be derived from Command's delay). Problems:
 | 
				
			||||||
 | 
					        # - How to trace tracks? (+ Source can change: caching log might sucks)
 | 
				
			||||||
 | 
					        # - if liquidsoap's source/request changes: remaining time goes higher,
 | 
				
			||||||
 | 
					        #   thus avoiding fetch
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # Approach: something like having a mean time, such as:
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # ```
 | 
				
			||||||
 | 
					        # source = stream.source
 | 
				
			||||||
 | 
					        # mean_time = source.air_time
 | 
				
			||||||
 | 
					        #           + min(next_track.timestamp, source.remaining)
 | 
				
			||||||
 | 
					        #           - (command.delay + 1)
 | 
				
			||||||
 | 
					        # trace_required = \/ source' != source
 | 
				
			||||||
 | 
					        #                  \/ source.uri' != source.uri
 | 
				
			||||||
 | 
					        #                  \/ now < mean_time
 | 
				
			||||||
 | 
					        # ```
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
        source = self.streamer.source
 | 
					        source = self.streamer.source
 | 
				
			||||||
        if source and source.uri:
 | 
					        if source and source.uri:
 | 
				
			||||||
            log = self.trace_sound(source)
 | 
					            log = self.trace_sound(source)
 | 
				
			||||||
@ -102,21 +132,22 @@ class Monitor:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def trace_sound(self, source):
 | 
					    def trace_sound(self, source):
 | 
				
			||||||
        """ Return on air sound log (create if not present). """
 | 
					        """ Return on air sound log (create if not present). """
 | 
				
			||||||
        sound_path, air_time = source.uri, source.air_time
 | 
					        air_uri, air_time = source.uri, source.air_time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # check if there is yet a log for this sound on the source
 | 
					        # check if there is yet a log for this sound on the source
 | 
				
			||||||
        delta = tz.timedelta(seconds=5)
 | 
					        log = self.logs.on_air().filter(
 | 
				
			||||||
        air_times = (air_time - delta, air_time + delta)
 | 
					            Q(sound__path=air_uri) |
 | 
				
			||||||
 | 
					            # sound can be null when arbitrary sound file is played
 | 
				
			||||||
        log = self.logs.on_air().filter(source=source.id,
 | 
					            Q(sound__isnull=True, track__isnull=True, comment=air_uri),
 | 
				
			||||||
                                        sound__path=sound_path,
 | 
					            source=source.id,
 | 
				
			||||||
                                        date__range=air_times).first()
 | 
					            date__range=date_range(air_time, self.delay),
 | 
				
			||||||
 | 
					        ).first()
 | 
				
			||||||
        if log:
 | 
					        if log:
 | 
				
			||||||
            return log
 | 
					            return log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # get sound
 | 
					        # get sound
 | 
				
			||||||
        diff = None
 | 
					        diff = None
 | 
				
			||||||
        sound = Sound.objects.filter(path=sound_path).first()
 | 
					        sound = Sound.objects.filter(path=air_uri).first()
 | 
				
			||||||
        if sound and sound.episode_id is not None:
 | 
					        if sound and sound.episode_id is not None:
 | 
				
			||||||
            diff = Diffusion.objects.episode(id=sound.episode_id).on_air() \
 | 
					            diff = Diffusion.objects.episode(id=sound.episode_id).on_air() \
 | 
				
			||||||
                                    .now(air_time).first()
 | 
					                                    .now(air_time).first()
 | 
				
			||||||
@ -124,7 +155,7 @@ class Monitor:
 | 
				
			|||||||
        # log sound on air
 | 
					        # log sound on air
 | 
				
			||||||
        return self.log(type=Log.Type.on_air, date=source.air_time,
 | 
					        return self.log(type=Log.Type.on_air, date=source.air_time,
 | 
				
			||||||
                        source=source.id, sound=sound, diffusion=diff,
 | 
					                        source=source.id, sound=sound, diffusion=diff,
 | 
				
			||||||
                        comment=sound_path)
 | 
					                        comment=air_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def trace_tracks(self, log):
 | 
					    def trace_tracks(self, log):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -155,19 +186,56 @@ class Monitor:
 | 
				
			|||||||
        Handle scheduled diffusion, trigger if needed, preload playlists
 | 
					        Handle scheduled diffusion, trigger if needed, preload playlists
 | 
				
			||||||
        and so on.
 | 
					        and so on.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # TODO: restart
 | 
					        # TODO: program restart
 | 
				
			||||||
        # TODO: handle conflict + cancel
 | 
					
 | 
				
			||||||
        diff = Diffusion.objects.station(self.station).on_air().now() \
 | 
					        # Diffusion conflicts are handled by the way a diffusion is defined
 | 
				
			||||||
 | 
					        # as candidate for the next dealer's start.
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # ```
 | 
				
			||||||
 | 
					        # logged_diff: /\ \A diff in diffs: \E log: /\ log.type = START
 | 
				
			||||||
 | 
					        #                                           /\ log.diff = diff
 | 
				
			||||||
 | 
					        #                                           /\ log.date = diff.start
 | 
				
			||||||
 | 
					        # queue_empty: /\ dealer.queue is empty
 | 
				
			||||||
 | 
					        #              /\ \/ ~dealer.on_air
 | 
				
			||||||
 | 
					        #                 \/ dealer.remaining < delay
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # start_allowed: /\ diff not in logged_diff
 | 
				
			||||||
 | 
					        #                /\ queue_empty
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # start_canceled: /\ diff not in logged diff
 | 
				
			||||||
 | 
					        #                 /\ ~queue_empty
 | 
				
			||||||
 | 
					        #                 /\ diff.start < now + cancel_timeout
 | 
				
			||||||
 | 
					        # ```
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        now = tz.now()
 | 
				
			||||||
 | 
					        diff = Diffusion.objects.station(self.station).on_air().now(now) \
 | 
				
			||||||
                        .filter(episode__sound__type=Sound.Type.archive) \
 | 
					                        .filter(episode__sound__type=Sound.Type.archive) \
 | 
				
			||||||
                        .first()
 | 
					                        .first()
 | 
				
			||||||
        log = self.logs.start().filter(diffusion=diff) if diff else None
 | 
					        # 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:
 | 
					        if not diff or log:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        playlist = Sound.objects.episode(id=diff.episode_id).paths()
 | 
					 | 
				
			||||||
        dealer = self.streamer.dealer
 | 
					        dealer = self.streamer.dealer
 | 
				
			||||||
        dealer.queue(*playlist)
 | 
					        # start
 | 
				
			||||||
        self.log(type=Log.Type.start, source=dealer.id, diffusion=diff,
 | 
					        if not dealer.queue and dealer.rid is None or \
 | 
				
			||||||
 | 
					                dealer.remaining < self.delay.total_seconds():
 | 
				
			||||||
 | 
					            self.start_diff(dealer, diff)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # cancel
 | 
				
			||||||
 | 
					        if diff.start < now - self.cancel_timeout:
 | 
				
			||||||
 | 
					            self.cancel_diff(dealer, diff)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def start_diff(self, source, diff):
 | 
				
			||||||
 | 
					        playlist = Sound.objects.episode(id=diff.episode_id).paths()
 | 
				
			||||||
 | 
					        source.append(*playlist)
 | 
				
			||||||
 | 
					        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))
 | 
					                 comment=str(diff))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def sync(self):
 | 
					    def sync(self):
 | 
				
			||||||
@ -207,7 +275,8 @@ class Command (BaseCommand):
 | 
				
			|||||||
            '-d', '--delay', type=int,
 | 
					            '-d', '--delay', type=int,
 | 
				
			||||||
            default=1000,
 | 
					            default=1000,
 | 
				
			||||||
            help='time to sleep in MILLISECONDS between two updates when we '
 | 
					            help='time to sleep in MILLISECONDS between two updates when we '
 | 
				
			||||||
                 'monitor'
 | 
					                 'monitor. This influence the delay before a diffusion is '
 | 
				
			||||||
 | 
					                 'launched.'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        group.add_argument(
 | 
					        group.add_argument(
 | 
				
			||||||
            '-s', '--station', type=str, action='append',
 | 
					            '-s', '--station', type=str, action='append',
 | 
				
			||||||
@ -215,7 +284,7 @@ class Command (BaseCommand):
 | 
				
			|||||||
                 'all stations'
 | 
					                 'all stations'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        group.add_argument(
 | 
					        group.add_argument(
 | 
				
			||||||
            '-t', '--timeout', type=int,
 | 
					            '-t', '--timeout', type=float,
 | 
				
			||||||
            default=Monitor.cancel_timeout,
 | 
					            default=Monitor.cancel_timeout,
 | 
				
			||||||
            help='time to wait in MINUTES before canceling a diffusion that '
 | 
					            help='time to wait in MINUTES before canceling a diffusion that '
 | 
				
			||||||
                 'should have ran but did not. '
 | 
					                 'should have ran but did not. '
 | 
				
			||||||
@ -226,7 +295,6 @@ class Command (BaseCommand):
 | 
				
			|||||||
               config=None, run=None, monitor=None,
 | 
					               config=None, run=None, monitor=None,
 | 
				
			||||||
               station=[], delay=1000, timeout=600,
 | 
					               station=[], delay=1000, timeout=600,
 | 
				
			||||||
               **options):
 | 
					               **options):
 | 
				
			||||||
 | 
					 | 
				
			||||||
        stations = Station.objects.filter(name__in=station) if station else \
 | 
					        stations = Station.objects.filter(name__in=station) if station else \
 | 
				
			||||||
                   Station.objects.all()
 | 
					                   Station.objects.all()
 | 
				
			||||||
        streamers = [Streamer(station) for station in stations]
 | 
					        streamers = [Streamer(station) for station in stations]
 | 
				
			||||||
@ -238,14 +306,15 @@ class Command (BaseCommand):
 | 
				
			|||||||
                streamer.run_process()
 | 
					                streamer.run_process()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if monitor:
 | 
					        if monitor:
 | 
				
			||||||
            monitors = [Monitor(streamer, cancel_timeout=timeout)
 | 
					            delay = tz.timedelta(milliseconds=delay)
 | 
				
			||||||
 | 
					            timeout = tz.timedelta(minutes=timeout)
 | 
				
			||||||
 | 
					            monitors = [Monitor(streamer, delay, timeout)
 | 
				
			||||||
                        for streamer in streamers]
 | 
					                        for streamer in streamers]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            delay = delay / 1000
 | 
					            while not run or streamer.is_running:
 | 
				
			||||||
            while True:
 | 
					 | 
				
			||||||
                for monitor in monitors:
 | 
					                for monitor in monitors:
 | 
				
			||||||
                    monitor.monitor()
 | 
					                    monitor.monitor()
 | 
				
			||||||
                time.sleep(delay)
 | 
					                time.sleep(delay.total_seconds())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if run:
 | 
					        if run:
 | 
				
			||||||
            for streamer in streamers:
 | 
					            for streamer in streamers:
 | 
				
			||||||
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@ -122,7 +122,7 @@ class Diffusion(BaseRerun):
 | 
				
			|||||||
    class Type(IntEnum):
 | 
					    class Type(IntEnum):
 | 
				
			||||||
        on_air = 0x00
 | 
					        on_air = 0x00
 | 
				
			||||||
        unconfirmed = 0x01
 | 
					        unconfirmed = 0x01
 | 
				
			||||||
        canceled = 0x02
 | 
					        cancel = 0x02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    episode = models.ForeignKey(
 | 
					    episode = models.ForeignKey(
 | 
				
			||||||
        Episode, models.CASCADE,
 | 
					        Episode, models.CASCADE,
 | 
				
			||||||
 | 
				
			|||||||
@ -167,18 +167,14 @@ class Log(models.Model):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Source has been stopped, e.g. manually
 | 
					        Source has been stopped, e.g. manually
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        # Rule: \/ diffusion != null \/ sound != null
 | 
				
			||||||
        start = 0x01
 | 
					        start = 0x01
 | 
				
			||||||
        """
 | 
					        """ Diffusion or sound has been request to be played. """
 | 
				
			||||||
        The diffusion or sound has been triggered by the streamer or
 | 
					        cancel = 0x02
 | 
				
			||||||
        manually.
 | 
					        """ Diffusion has been canceled. """
 | 
				
			||||||
        """
 | 
					        # Rule: \/ sound != null /\ track == null
 | 
				
			||||||
        load = 0x02
 | 
					        #       \/ sound == null /\ track != null
 | 
				
			||||||
        """
 | 
					        #       \/ sound == null /\ track == null /\ comment = sound_path
 | 
				
			||||||
        A playlist has updated, and loading started. A related Diffusion
 | 
					 | 
				
			||||||
        does not means that the playlist is only for it (e.g. after a
 | 
					 | 
				
			||||||
        crash, it can reload previous remaining sound files + thoses of
 | 
					 | 
				
			||||||
        the next diffusion)
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        on_air = 0x03
 | 
					        on_air = 0x03
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        The sound or diffusion has been detected occurring on air. Can
 | 
					        The sound or diffusion has been detected occurring on air. Can
 | 
				
			||||||
@ -186,9 +182,7 @@ class Log(models.Model):
 | 
				
			|||||||
        them since they don't have an attached sound archive.
 | 
					        them since they don't have an attached sound archive.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        other = 0x04
 | 
					        other = 0x04
 | 
				
			||||||
        """
 | 
					        """ Other log """
 | 
				
			||||||
        Other log
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    station = models.ForeignKey(
 | 
					    station = models.ForeignKey(
 | 
				
			||||||
        Station, models.CASCADE,
 | 
					        Station, models.CASCADE,
 | 
				
			||||||
@ -216,12 +210,6 @@ class Log(models.Model):
 | 
				
			|||||||
        max_length=512, blank=True, null=True,
 | 
					        max_length=512, blank=True, null=True,
 | 
				
			||||||
        verbose_name=_('comment'),
 | 
					        verbose_name=_('comment'),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					 | 
				
			||||||
    diffusion = models.ForeignKey(
 | 
					 | 
				
			||||||
        Diffusion, models.SET_NULL,
 | 
					 | 
				
			||||||
        blank=True, null=True, db_index=True,
 | 
					 | 
				
			||||||
        verbose_name=_('Diffusion'),
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    sound = models.ForeignKey(
 | 
					    sound = models.ForeignKey(
 | 
				
			||||||
        Sound, models.SET_NULL,
 | 
					        Sound, models.SET_NULL,
 | 
				
			||||||
        blank=True, null=True, db_index=True,
 | 
					        blank=True, null=True, db_index=True,
 | 
				
			||||||
@ -232,6 +220,11 @@ class Log(models.Model):
 | 
				
			|||||||
        blank=True, null=True, db_index=True,
 | 
					        blank=True, null=True, db_index=True,
 | 
				
			||||||
        verbose_name=_('Track'),
 | 
					        verbose_name=_('Track'),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    diffusion = models.ForeignKey(
 | 
				
			||||||
 | 
					        Diffusion, models.SET_NULL,
 | 
				
			||||||
 | 
					        blank=True, null=True, db_index=True,
 | 
				
			||||||
 | 
					        verbose_name=_('Diffusion'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    objects = LogQuerySet.as_manager()
 | 
					    objects = LogQuerySet.as_manager()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -42,6 +42,11 @@ def interactive (id, s) =
 | 
				
			|||||||
                    description="Seek to a relative position",
 | 
					                    description="Seek to a relative position",
 | 
				
			||||||
                    usage="seek <duration>",
 | 
					                    usage="seek <duration>",
 | 
				
			||||||
                    "seek", fun (x) ->  begin seek(s, x) end)
 | 
					                    "seek", fun (x) ->  begin seek(s, x) end)
 | 
				
			||||||
 | 
					    server.register(namespace=id,
 | 
				
			||||||
 | 
					                    description="Get source's track remaining time",
 | 
				
			||||||
 | 
					                    usage="remaining",
 | 
				
			||||||
 | 
					                    "remaining", fun (_) ->  begin json_of(source.remaining(s)) end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    s = store_metadata(id=id, size=1, s)
 | 
					    s = store_metadata(id=id, size=1, s)
 | 
				
			||||||
    add_skip_command(s)
 | 
					    add_skip_command(s)
 | 
				
			||||||
    s
 | 
					    s
 | 
				
			||||||
 | 
				
			|||||||
@ -2,16 +2,18 @@ import datetime
 | 
				
			|||||||
import django.utils.timezone as tz
 | 
					import django.utils.timezone as tz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def date_range(date):
 | 
					def date_range(date, delta=None, **delta_kwargs):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    Return a range of provided date such as `[date-delta, date+delta]`.
 | 
				
			||||||
 | 
					    :param date: the reference date
 | 
				
			||||||
 | 
					    :param delta: timedelta
 | 
				
			||||||
 | 
					    :param \**delta_kwargs: timedelta init arguments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Return a datetime range for a given day, as:
 | 
					    Return a datetime range for a given day, as:
 | 
				
			||||||
    ```(date, 0:0:0:0; date, 23:59:59:999)```.
 | 
					    ```(date, 0:0:0:0; date, 23:59:59:999)```.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    date = date_or_default(date, tz.datetime)
 | 
					    delta = tz.timedelta(**delta_kwargs) if delta is None else delta
 | 
				
			||||||
    return (
 | 
					    return [date - delta, date + delta]
 | 
				
			||||||
        date.replace(hour=0, minute=0, second=0),
 | 
					 | 
				
			||||||
        date.replace(hour=23, minute=59, second=59, microsecond=999)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def cast_date(date, into=datetime.date):
 | 
					def cast_date(date, into=datetime.date):
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								setup.py
									
									
									
									
									
								
							@ -15,12 +15,12 @@ def to_array (path):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
setup(
 | 
					setup(
 | 
				
			||||||
    name='aircox',
 | 
					    name='aircox',
 | 
				
			||||||
    version='0.1',
 | 
					    version='0.9',
 | 
				
			||||||
    license='GPLv3',
 | 
					    license='GPLv3',
 | 
				
			||||||
    author='bkfox',
 | 
					    author='bkfox',
 | 
				
			||||||
    description='Aircox is a radio programs manager that includes tools and cms',
 | 
					    description='Aircox is a radio programs manager including tools and cms',
 | 
				
			||||||
    long_description=to_rst('README.md'),
 | 
					    long_description=to_rst('README.md'),
 | 
				
			||||||
    url='http://bkfox.net/',
 | 
					    url='https://github.com/bkfox/aircox',
 | 
				
			||||||
    packages=find_packages(),
 | 
					    packages=find_packages(),
 | 
				
			||||||
    include_package_data=True,
 | 
					    include_package_data=True,
 | 
				
			||||||
    install_requires=to_array('requirements.txt'),
 | 
					    install_requires=to_array('requirements.txt'),
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user