fix some issues, make the liquidsoap monitor working
This commit is contained in:
		@ -1,10 +1,7 @@
 | 
			
		||||
"""
 | 
			
		||||
Control Liquidsoap
 | 
			
		||||
"""
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import datetime
 | 
			
		||||
import collections
 | 
			
		||||
import time
 | 
			
		||||
from argparse import RawTextHelpFormatter
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand, CommandError
 | 
			
		||||
@ -14,74 +11,6 @@ import aircox_liquidsoap.settings as settings
 | 
			
		||||
import aircox_liquidsoap.utils as utils
 | 
			
		||||
import aircox_programs.models as models
 | 
			
		||||
 | 
			
		||||
class DiffusionInfo:
 | 
			
		||||
    date = None
 | 
			
		||||
    original = None
 | 
			
		||||
    sounds = None
 | 
			
		||||
    duration = 0
 | 
			
		||||
 | 
			
		||||
    def __init__ (self, diffusion):
 | 
			
		||||
        episode = diffusion.episode
 | 
			
		||||
        self.original = diffusion
 | 
			
		||||
        self.sounds = [ sound for sound in episode.sounds
 | 
			
		||||
                            if sound.type = models.Sound.Type['archive'] ]
 | 
			
		||||
        self.sounds.sort(key = 'path')
 | 
			
		||||
        self.date = diffusion.date
 | 
			
		||||
        self.duration = episode.get_duration()
 | 
			
		||||
        self.end = self.date + tz.datetime.timedelta(seconds = self.duration)
 | 
			
		||||
 | 
			
		||||
    def __eq___ (self, info):
 | 
			
		||||
        return self.original.id == info.original.id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ControllerMonitor:
 | 
			
		||||
    current = None
 | 
			
		||||
    queue = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_next (self, controller):
 | 
			
		||||
        upcoming = models.Diffusion.get_next(
 | 
			
		||||
            station = controller.station,
 | 
			
		||||
            # diffusion__episode__not blank
 | 
			
		||||
            # diffusion__episode__sounds not blank
 | 
			
		||||
        )
 | 
			
		||||
        return Monitor.Info(upcoming[0]) if upcoming else None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def playlist (self, controller):
 | 
			
		||||
        dealer = controller.dealer
 | 
			
		||||
        on_air = dealer.current_sound
 | 
			
		||||
        playlist = dealer.playlist
 | 
			
		||||
 | 
			
		||||
        next = self.queue[0]
 | 
			
		||||
 | 
			
		||||
        # last track: time to reload playlist
 | 
			
		||||
        if on_air == playlist[-1] or on_air not in playlist:
 | 
			
		||||
            dealer.playlist = [sound.path for sound in next.sounds]
 | 
			
		||||
            dealer.on = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def current (self, controller):
 | 
			
		||||
        # time to switch...
 | 
			
		||||
        if on_air not in self.current.sounds:
 | 
			
		||||
            self.current = self.queue.popleft()
 | 
			
		||||
 | 
			
		||||
        if self.current.date <= tz.datetime.now() and not dealer.on:
 | 
			
		||||
            dealer.on = True
 | 
			
		||||
            print('start ', self.current.original)
 | 
			
		||||
 | 
			
		||||
        # HERE
 | 
			
		||||
 | 
			
		||||
        upcoming = self.get_next(controller)
 | 
			
		||||
 | 
			
		||||
        if upcoming.date <= tz.datetime.now() and not self.current:
 | 
			
		||||
            self.current = upcoming
 | 
			
		||||
 | 
			
		||||
        if not self.upcoming or upcoming != self.upcoming:
 | 
			
		||||
            dealer.playlist = [sound.path for sound in upcomming.sounds]
 | 
			
		||||
            dealer.on = False
 | 
			
		||||
            self.upcoming = upcoming
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command (BaseCommand):
 | 
			
		||||
    help= __doc__
 | 
			
		||||
@ -98,24 +27,27 @@ class Command (BaseCommand):
 | 
			
		||||
            help='Runs in monitor mode'
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            '-s', '--sleep', type=int,
 | 
			
		||||
            default=1,
 | 
			
		||||
            help='Time to sleep before update'
 | 
			
		||||
            '-d', '--delay', type=int,
 | 
			
		||||
            default=1000,
 | 
			
		||||
            help='Time to sleep in milliseconds before update on monitor'
 | 
			
		||||
        )
 | 
			
		||||
        # start and run liquidsoap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def handle (self, *args, **options):
 | 
			
		||||
        connector = utils.Connector()
 | 
			
		||||
        self.monitor = utils.Monitor()
 | 
			
		||||
        self.monitor = utils.Monitor(connector)
 | 
			
		||||
        self.monitor.update()
 | 
			
		||||
 | 
			
		||||
        if options.get('on_air'):
 | 
			
		||||
            for id, controller in self.monitor.controller.items():
 | 
			
		||||
                print(id, controller.master.current_sound())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if options.get('monitor'):
 | 
			
		||||
            sleep = 
 | 
			
		||||
            delay = options.get('delay') / 1000
 | 
			
		||||
            while True:
 | 
			
		||||
                for controller in self.monitor.controllers.values():
 | 
			
		||||
                    controller.dealer.monitor()
 | 
			
		||||
                time.sleep(delay)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,6 @@
 | 
			
		||||
{# Utilities #}
 | 
			
		||||
def interactive_source (id, s, ) = \
 | 
			
		||||
    def apply_metadata(m) = \
 | 
			
		||||
        m = json_of(compact=true, m) \
 | 
			
		||||
        ignore(interactive.string('#{id}_meta', m)) \
 | 
			
		||||
    end \
 | 
			
		||||
    \
 | 
			
		||||
    s = on_metadata(id = id, apply_metadata, s) \
 | 
			
		||||
    s = store_metadata(id=id, size=1, s) \
 | 
			
		||||
    add_skip_command(s) \
 | 
			
		||||
    s \
 | 
			
		||||
end \
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import re
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
 | 
			
		||||
from aircox_programs.utils import to_timedelta
 | 
			
		||||
import aircox_programs.models as models
 | 
			
		||||
@ -50,10 +51,10 @@ class Connector:
 | 
			
		||||
        data = bytes(''.join([str(d) for d in data]) + '\n', encoding='utf-8')
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            reg = re.compile('(.*)[\n\r]+END[\n\r]*$')
 | 
			
		||||
            reg = re.compile(r'(.*)\s+END\s*$')
 | 
			
		||||
            self.__socket.sendall(data)
 | 
			
		||||
            data = ''
 | 
			
		||||
            while not reg.match(data):
 | 
			
		||||
            while not reg.search(data):
 | 
			
		||||
                data += self.__socket.recv(1024).decode('unicode_escape')
 | 
			
		||||
 | 
			
		||||
            if data:
 | 
			
		||||
@ -166,7 +167,7 @@ class Source:
 | 
			
		||||
    @property
 | 
			
		||||
    def current_sound (self):
 | 
			
		||||
        self.update()
 | 
			
		||||
        self.metadata['initial_uri']
 | 
			
		||||
        self.metadata.get('initial_uri')
 | 
			
		||||
 | 
			
		||||
    def stream_info (self):
 | 
			
		||||
        """
 | 
			
		||||
@ -201,15 +202,29 @@ class Source:
 | 
			
		||||
 | 
			
		||||
        Return -1 in case no update happened
 | 
			
		||||
        """
 | 
			
		||||
        if metadata:
 | 
			
		||||
        if metadata is not None:
 | 
			
		||||
            source = metadata.get('source') or ''
 | 
			
		||||
            if self.program and not source.startswith(self.id):
 | 
			
		||||
                return -1
 | 
			
		||||
            self.metadata = metadata
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        r = self.connector.send('var.get ', self.id + '_meta', parse_json=True)
 | 
			
		||||
        return self.update(metadata = r) if r else -1
 | 
			
		||||
        # r = self.connector.send('var.get ', self.id + '_meta', parse_json=True)
 | 
			
		||||
        r = self.connector.send(self.id, '.get', parse=True)
 | 
			
		||||
        return self.update(metadata = r or {})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Master (Source):
 | 
			
		||||
    """
 | 
			
		||||
    A master Source
 | 
			
		||||
    """
 | 
			
		||||
    def update (self, metadata = None):
 | 
			
		||||
        if metadata is not None:
 | 
			
		||||
            return super().update(metadata)
 | 
			
		||||
 | 
			
		||||
        r = self.connector.send('request.on_air')
 | 
			
		||||
        r = self.connector.send('request.metadata ', r, parse = True)
 | 
			
		||||
        return self.update(metadata = r or {})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Dealer (Source):
 | 
			
		||||
@ -221,7 +236,7 @@ class Dealer (Source):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def id (self):
 | 
			
		||||
        return self.station.name + '_dealer'
 | 
			
		||||
        return self.station.slug + '_dealer'
 | 
			
		||||
 | 
			
		||||
    def stream_info (self):
 | 
			
		||||
        pass
 | 
			
		||||
@ -257,31 +272,24 @@ class Dealer (Source):
 | 
			
		||||
            file.write('\n'.join(sounds))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def __get_queue (self, date):
 | 
			
		||||
    def __get_next (self, date, on_air):
 | 
			
		||||
        """
 | 
			
		||||
        Return a list of diffusion candidates of being running right now.
 | 
			
		||||
        Add an attribute "sounds" with the episode's archives.
 | 
			
		||||
        Return which diffusion should be played now and not playing
 | 
			
		||||
        """
 | 
			
		||||
        r = [ models.Diffusion.get_prev(self.station, date),
 | 
			
		||||
              models.Diffusion.get_next(self.station, date) ]
 | 
			
		||||
        r = [ diffusion.prefetch_related('episode__sounds')[0]
 | 
			
		||||
        r = [ diffusion.prefetch_related('sounds')[0]
 | 
			
		||||
                for diffusion in r if diffusion.count() ]
 | 
			
		||||
        for diffusion in r:
 | 
			
		||||
            setattr(diffusion, 'sounds',
 | 
			
		||||
                    [ sound.path for sound in diffusion.get_sounds() ])
 | 
			
		||||
        return r
 | 
			
		||||
 | 
			
		||||
    def __what_now (self, date, on_air, queue):
 | 
			
		||||
        """
 | 
			
		||||
        Return which diffusion is on_air from the given queue
 | 
			
		||||
        """
 | 
			
		||||
        for diffusion in queue:
 | 
			
		||||
            duration = diffusion.archives_duration()
 | 
			
		||||
            end_at = diffusion.date + tz.timedelta(seconds = diffusion.archives_duration())
 | 
			
		||||
        for diffusion in r:
 | 
			
		||||
            duration = to_timedelta(diffusion.archives_duration())
 | 
			
		||||
            end_at = diffusion.date + duration
 | 
			
		||||
            if end_at < date:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if diffusion.sounds and on_air in diffusion.sounds:
 | 
			
		||||
            diffusion.playlist = [ sound.path
 | 
			
		||||
                                    for sound in diffusion.get_archives() ]
 | 
			
		||||
            if diffusion.playlist and on_air not in diffusion.playlist:
 | 
			
		||||
                return diffusion
 | 
			
		||||
 | 
			
		||||
    def monitor (self):
 | 
			
		||||
@ -289,12 +297,25 @@ class Dealer (Source):
 | 
			
		||||
        Monitor playlist (if it is time to load) and if it time to trigger
 | 
			
		||||
        the button to start a diffusion.
 | 
			
		||||
        """
 | 
			
		||||
        on_air = self.current_soudn
 | 
			
		||||
        playlist = self.playlist
 | 
			
		||||
        on_air = self.current_sound
 | 
			
		||||
        now = tz.make_aware(tz.datetime.now())
 | 
			
		||||
 | 
			
		||||
        queue = self.__get_queue()
 | 
			
		||||
        current_diffusion = self.__what_now()
 | 
			
		||||
        diff = self.__get_next(now, on_air)
 | 
			
		||||
        if not diff:
 | 
			
		||||
            return # there is nothing we can do
 | 
			
		||||
 | 
			
		||||
        # playlist reload
 | 
			
		||||
        if self.playlist != diff.playlist:
 | 
			
		||||
            if not playlist or on_air == playlist[-1] or \
 | 
			
		||||
                on_air not in playlist:
 | 
			
		||||
                self.on = False
 | 
			
		||||
                self.playlist = diff.playlist
 | 
			
		||||
 | 
			
		||||
        # run the diff
 | 
			
		||||
        if self.playlist == diff.playlist and diff.date <= now:
 | 
			
		||||
            # FIXME: log
 | 
			
		||||
            self.on = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Controller:
 | 
			
		||||
@ -324,7 +345,7 @@ class Controller:
 | 
			
		||||
        self.station = station
 | 
			
		||||
        self.station.controller = self
 | 
			
		||||
 | 
			
		||||
        self.master = Source(self)
 | 
			
		||||
        self.master = Master(self)
 | 
			
		||||
        self.dealer = Dealer(self)
 | 
			
		||||
        self.streams = {
 | 
			
		||||
            source.id : source
 | 
			
		||||
 | 
			
		||||
@ -53,6 +53,7 @@ class SoundAdmin (NameableAdmin):
 | 
			
		||||
        (None, { 'fields': ['embed', 'duration', 'mtime'] }),
 | 
			
		||||
        (None, { 'fields': ['removed', 'good_quality', 'public' ] } )
 | 
			
		||||
    ]
 | 
			
		||||
    readonly_fields = ('path', 'duration',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Stream)
 | 
			
		||||
@ -82,7 +83,7 @@ class ProgramAdmin (NameableAdmin):
 | 
			
		||||
@admin.register(Diffusion)
 | 
			
		||||
class DiffusionAdmin (admin.ModelAdmin):
 | 
			
		||||
    def archives (self, obj):
 | 
			
		||||
        sounds = obj.get_archives()
 | 
			
		||||
        sounds = [ str(s) for s in obj.get_archives()]
 | 
			
		||||
        return ', '.join(sounds) if sounds else ''
 | 
			
		||||
 | 
			
		||||
    list_display = ('id', 'type', 'date', 'archives', 'program', 'initial')
 | 
			
		||||
 | 
			
		||||
@ -18,10 +18,12 @@ Where:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
To check quality of files, call the command sound_quality_check using the
 | 
			
		||||
parameters given by the setting AIRCOX_SOUND_QUALITY.
 | 
			
		||||
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires
 | 
			
		||||
Sox (and soxi).
 | 
			
		||||
"""
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import subprocess
 | 
			
		||||
from argparse import RawTextHelpFormatter
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand, CommandError
 | 
			
		||||
@ -53,13 +55,19 @@ class Command (BaseCommand):
 | 
			
		||||
                 ' matching episode on sounds that have not been yet assigned'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def handle (self, *args, **options):
 | 
			
		||||
        if options.get('scan'):
 | 
			
		||||
            self.scan()
 | 
			
		||||
        if options.get('quality_check'):
 | 
			
		||||
            self.check_quality(check = (not options.get('scan')) )
 | 
			
		||||
 | 
			
		||||
    def _get_duration (self, path):
 | 
			
		||||
        p = subprocess.Popen(['soxi', '-D', path], stdout=subprocess.PIPE,
 | 
			
		||||
                             stderr=subprocess.PIPE)
 | 
			
		||||
        out, err = p.communicate()
 | 
			
		||||
        if not err:
 | 
			
		||||
            return utils.seconds_to_time(int(float(out)))
 | 
			
		||||
 | 
			
		||||
    def get_sound_info (self, program, path):
 | 
			
		||||
        """
 | 
			
		||||
        Parse file name to get info on the assumption it has the correct
 | 
			
		||||
@ -82,6 +90,7 @@ class Command (BaseCommand):
 | 
			
		||||
        else:
 | 
			
		||||
            r = r.groupdict()
 | 
			
		||||
 | 
			
		||||
        r['duration'] = self._get_duration(path)
 | 
			
		||||
        r['name'] = r['name'].replace('_', ' ').capitalize()
 | 
			
		||||
        r['path'] = path
 | 
			
		||||
        return r
 | 
			
		||||
@ -109,12 +118,18 @@ class Command (BaseCommand):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def check_sounds (qs):
 | 
			
		||||
        """
 | 
			
		||||
        Only check for the sound existence or update
 | 
			
		||||
        """
 | 
			
		||||
        # check files
 | 
			
		||||
        for sound in qs:
 | 
			
		||||
            if sound.check_on_file():
 | 
			
		||||
                sound.save(check = False)
 | 
			
		||||
 | 
			
		||||
    def scan (self):
 | 
			
		||||
        """
 | 
			
		||||
        For all programs, scan dirs
 | 
			
		||||
        """
 | 
			
		||||
        print('scan files for all programs...')
 | 
			
		||||
        programs = Program.objects.filter()
 | 
			
		||||
 | 
			
		||||
@ -149,7 +164,8 @@ class Command (BaseCommand):
 | 
			
		||||
            sound_info = self.get_sound_info(program, path)
 | 
			
		||||
            sound = Sound.objects.get_or_create(
 | 
			
		||||
                path = path,
 | 
			
		||||
                defaults = { 'name': sound_info['name'] }
 | 
			
		||||
                defaults = { 'name': sound_info['name'],
 | 
			
		||||
                             'duration': sound_info['duration'] or None }
 | 
			
		||||
            )[0]
 | 
			
		||||
            sound.__dict__.update(sound_kwargs)
 | 
			
		||||
            sound.save(check = False)
 | 
			
		||||
 | 
			
		||||
@ -49,9 +49,8 @@ class Stats:
 | 
			
		||||
 | 
			
		||||
        args.append('stats')
 | 
			
		||||
 | 
			
		||||
        p = subprocess.Popen(args,
 | 
			
		||||
                             stdout=subprocess.PIPE,
 | 
			
		||||
                             stderr = subprocess.PIPE)
 | 
			
		||||
        p = subprocess.Popen(args, stdout=subprocess.PIPE,
 | 
			
		||||
                             stderr=subprocess.PIPE)
 | 
			
		||||
        # sox outputs to stderr (my god WHYYYY)
 | 
			
		||||
        out_, out = p.communicate()
 | 
			
		||||
        self.parse(str(out, encoding='utf-8'))
 | 
			
		||||
 | 
			
		||||
@ -564,16 +564,17 @@ class Diffusion (models.Model):
 | 
			
		||||
        Get total duration of the archives. May differ from the schedule
 | 
			
		||||
        duration.
 | 
			
		||||
        """
 | 
			
		||||
        return sum([ sound.duration for sound in self.sounds
 | 
			
		||||
                        if sound.type == Sound.Type['archive']])
 | 
			
		||||
        r = [ sound.duration
 | 
			
		||||
                for sound in self.sounds.filter(type = Sound.Type['archive'])
 | 
			
		||||
                if sound.duration ]
 | 
			
		||||
        return sum(r) or self.duration
 | 
			
		||||
 | 
			
		||||
    def get_archives (self):
 | 
			
		||||
        """
 | 
			
		||||
        Return an ordered list of archives sounds for the given episode.
 | 
			
		||||
        """
 | 
			
		||||
        r = [ sound for sound in self.sounds.all()
 | 
			
		||||
        r = [ sound for sound in self.sounds.all().order_by('path')
 | 
			
		||||
              if sound.type == Sound.Type['archive'] ]
 | 
			
		||||
        r.sort(key = 'path')
 | 
			
		||||
        return r
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ def to_timedelta (time):
 | 
			
		||||
    return datetime.timedelta(
 | 
			
		||||
        hours = time.hour,
 | 
			
		||||
        minutes = time.minute,
 | 
			
		||||
        seconds = time.seconds
 | 
			
		||||
        seconds = time.second
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user