fix some issues, make the liquidsoap monitor working

This commit is contained in:
bkfox 2015-11-23 02:04:37 +01:00
parent 25e3d4cb53
commit edfdd94eda
8 changed files with 88 additions and 123 deletions

View File

@ -1,10 +1,7 @@
""" """
Control Liquidsoap Control Liquidsoap
""" """
import os import time
import re
import datetime
import collections
from argparse import RawTextHelpFormatter from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError 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_liquidsoap.utils as utils
import aircox_programs.models as models 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): class Command (BaseCommand):
help= __doc__ help= __doc__
@ -98,24 +27,27 @@ class Command (BaseCommand):
help='Runs in monitor mode' help='Runs in monitor mode'
) )
parser.add_argument( parser.add_argument(
'-s', '--sleep', type=int, '-d', '--delay', type=int,
default=1, default=1000,
help='Time to sleep before update' help='Time to sleep in milliseconds before update on monitor'
) )
# start and run liquidsoap # start and run liquidsoap
def handle (self, *args, **options): def handle (self, *args, **options):
connector = utils.Connector() connector = utils.Connector()
self.monitor = utils.Monitor() self.monitor = utils.Monitor(connector)
self.monitor.update() self.monitor.update()
if options.get('on_air'): if options.get('on_air'):
for id, controller in self.monitor.controller.items(): for id, controller in self.monitor.controller.items():
print(id, controller.master.current_sound()) print(id, controller.master.current_sound())
if options.get('monitor'): 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)

View File

@ -1,11 +1,6 @@
{# Utilities #} {# Utilities #}
def interactive_source (id, s, ) = \ def interactive_source (id, s, ) = \
def apply_metadata(m) = \ s = store_metadata(id=id, size=1, s) \
m = json_of(compact=true, m) \
ignore(interactive.string('#{id}_meta', m)) \
end \
\
s = on_metadata(id = id, apply_metadata, s) \
add_skip_command(s) \ add_skip_command(s) \
s \ s \
end \ end \

View File

@ -4,6 +4,7 @@ import re
import json import json
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils import timezone as tz
from aircox_programs.utils import to_timedelta from aircox_programs.utils import to_timedelta
import aircox_programs.models as models 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') data = bytes(''.join([str(d) for d in data]) + '\n', encoding='utf-8')
try: try:
reg = re.compile('(.*)[\n\r]+END[\n\r]*$') reg = re.compile(r'(.*)\s+END\s*$')
self.__socket.sendall(data) self.__socket.sendall(data)
data = '' data = ''
while not reg.match(data): while not reg.search(data):
data += self.__socket.recv(1024).decode('unicode_escape') data += self.__socket.recv(1024).decode('unicode_escape')
if data: if data:
@ -166,7 +167,7 @@ class Source:
@property @property
def current_sound (self): def current_sound (self):
self.update() self.update()
self.metadata['initial_uri'] self.metadata.get('initial_uri')
def stream_info (self): def stream_info (self):
""" """
@ -201,15 +202,29 @@ class Source:
Return -1 in case no update happened Return -1 in case no update happened
""" """
if metadata: if metadata is not None:
source = metadata.get('source') or '' source = metadata.get('source') or ''
if self.program and not source.startswith(self.id): if self.program and not source.startswith(self.id):
return -1 return -1
self.metadata = metadata self.metadata = metadata
return return
r = self.connector.send('var.get ', self.id + '_meta', parse_json=True) # 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(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): class Dealer (Source):
@ -221,7 +236,7 @@ class Dealer (Source):
@property @property
def id (self): def id (self):
return self.station.name + '_dealer' return self.station.slug + '_dealer'
def stream_info (self): def stream_info (self):
pass pass
@ -257,31 +272,24 @@ class Dealer (Source):
file.write('\n'.join(sounds)) 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. Return which diffusion should be played now and not playing
Add an attribute "sounds" with the episode's archives.
""" """
r = [ models.Diffusion.get_prev(self.station, date), r = [ models.Diffusion.get_prev(self.station, date),
models.Diffusion.get_next(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 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): for diffusion in r:
""" duration = to_timedelta(diffusion.archives_duration())
Return which diffusion is on_air from the given queue end_at = diffusion.date + duration
"""
for diffusion in queue:
duration = diffusion.archives_duration()
end_at = diffusion.date + tz.timedelta(seconds = diffusion.archives_duration())
if end_at < date: if end_at < date:
continue 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 return diffusion
def monitor (self): def monitor (self):
@ -289,12 +297,25 @@ class Dealer (Source):
Monitor playlist (if it is time to load) and if it time to trigger Monitor playlist (if it is time to load) and if it time to trigger
the button to start a diffusion. the button to start a diffusion.
""" """
on_air = self.current_soudn
playlist = self.playlist playlist = self.playlist
on_air = self.current_sound
now = tz.make_aware(tz.datetime.now())
queue = self.__get_queue() diff = self.__get_next(now, on_air)
current_diffusion = self.__what_now() 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: class Controller:
@ -324,7 +345,7 @@ class Controller:
self.station = station self.station = station
self.station.controller = self self.station.controller = self
self.master = Source(self) self.master = Master(self)
self.dealer = Dealer(self) self.dealer = Dealer(self)
self.streams = { self.streams = {
source.id : source source.id : source

View File

@ -53,6 +53,7 @@ class SoundAdmin (NameableAdmin):
(None, { 'fields': ['embed', 'duration', 'mtime'] }), (None, { 'fields': ['embed', 'duration', 'mtime'] }),
(None, { 'fields': ['removed', 'good_quality', 'public' ] } ) (None, { 'fields': ['removed', 'good_quality', 'public' ] } )
] ]
readonly_fields = ('path', 'duration',)
@admin.register(Stream) @admin.register(Stream)
@ -82,7 +83,7 @@ class ProgramAdmin (NameableAdmin):
@admin.register(Diffusion) @admin.register(Diffusion)
class DiffusionAdmin (admin.ModelAdmin): class DiffusionAdmin (admin.ModelAdmin):
def archives (self, obj): def archives (self, obj):
sounds = obj.get_archives() sounds = [ str(s) for s in obj.get_archives()]
return ', '.join(sounds) if sounds else '' return ', '.join(sounds) if sounds else ''
list_display = ('id', 'type', 'date', 'archives', 'program', 'initial') list_display = ('id', 'type', 'date', 'archives', 'program', 'initial')

View File

@ -18,10 +18,12 @@ Where:
To check quality of files, call the command sound_quality_check using the 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 os
import re import re
import subprocess
from argparse import RawTextHelpFormatter from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError 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' ' matching episode on sounds that have not been yet assigned'
) )
def handle (self, *args, **options): def handle (self, *args, **options):
if options.get('scan'): if options.get('scan'):
self.scan() self.scan()
if options.get('quality_check'): if options.get('quality_check'):
self.check_quality(check = (not options.get('scan')) ) 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): def get_sound_info (self, program, path):
""" """
Parse file name to get info on the assumption it has the correct Parse file name to get info on the assumption it has the correct
@ -82,6 +90,7 @@ class Command (BaseCommand):
else: else:
r = r.groupdict() r = r.groupdict()
r['duration'] = self._get_duration(path)
r['name'] = r['name'].replace('_', ' ').capitalize() r['name'] = r['name'].replace('_', ' ').capitalize()
r['path'] = path r['path'] = path
return r return r
@ -109,12 +118,18 @@ class Command (BaseCommand):
@staticmethod @staticmethod
def check_sounds (qs): def check_sounds (qs):
"""
Only check for the sound existence or update
"""
# check files # check files
for sound in qs: for sound in qs:
if sound.check_on_file(): if sound.check_on_file():
sound.save(check = False) sound.save(check = False)
def scan (self): def scan (self):
"""
For all programs, scan dirs
"""
print('scan files for all programs...') print('scan files for all programs...')
programs = Program.objects.filter() programs = Program.objects.filter()
@ -149,7 +164,8 @@ class Command (BaseCommand):
sound_info = self.get_sound_info(program, path) sound_info = self.get_sound_info(program, path)
sound = Sound.objects.get_or_create( sound = Sound.objects.get_or_create(
path = path, path = path,
defaults = { 'name': sound_info['name'] } defaults = { 'name': sound_info['name'],
'duration': sound_info['duration'] or None }
)[0] )[0]
sound.__dict__.update(sound_kwargs) sound.__dict__.update(sound_kwargs)
sound.save(check = False) sound.save(check = False)

View File

@ -49,9 +49,8 @@ class Stats:
args.append('stats') args.append('stats')
p = subprocess.Popen(args, p = subprocess.Popen(args, stdout=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stderr = subprocess.PIPE)
# sox outputs to stderr (my god WHYYYY) # sox outputs to stderr (my god WHYYYY)
out_, out = p.communicate() out_, out = p.communicate()
self.parse(str(out, encoding='utf-8')) self.parse(str(out, encoding='utf-8'))

View File

@ -564,16 +564,17 @@ class Diffusion (models.Model):
Get total duration of the archives. May differ from the schedule Get total duration of the archives. May differ from the schedule
duration. duration.
""" """
return sum([ sound.duration for sound in self.sounds r = [ sound.duration
if sound.type == Sound.Type['archive']]) for sound in self.sounds.filter(type = Sound.Type['archive'])
if sound.duration ]
return sum(r) or self.duration
def get_archives (self): def get_archives (self):
""" """
Return an ordered list of archives sounds for the given episode. 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'] ] if sound.type == Sound.Type['archive'] ]
r.sort(key = 'path')
return r return r
@classmethod @classmethod

View File

@ -8,7 +8,7 @@ def to_timedelta (time):
return datetime.timedelta( return datetime.timedelta(
hours = time.hour, hours = time.hour,
minutes = time.minute, minutes = time.minute,
seconds = time.seconds seconds = time.second
) )