diff --git a/aircox_liquidsoap/management/commands/liquidsoap.py b/aircox_liquidsoap/management/commands/liquidsoap.py index 1c698ed..9c73cf4 100644 --- a/aircox_liquidsoap/management/commands/liquidsoap.py +++ b/aircox_liquidsoap/management/commands/liquidsoap.py @@ -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) diff --git a/aircox_liquidsoap/templates/aircox_liquidsoap/config.liq b/aircox_liquidsoap/templates/aircox_liquidsoap/config.liq index 986fee4..1cb6863 100644 --- a/aircox_liquidsoap/templates/aircox_liquidsoap/config.liq +++ b/aircox_liquidsoap/templates/aircox_liquidsoap/config.liq @@ -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 \ diff --git a/aircox_liquidsoap/utils.py b/aircox_liquidsoap/utils.py index 9730f71..5f06d11 100644 --- a/aircox_liquidsoap/utils.py +++ b/aircox_liquidsoap/utils.py @@ -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 diff --git a/aircox_programs/admin.py b/aircox_programs/admin.py index 5b9f75f..ea428d2 100755 --- a/aircox_programs/admin.py +++ b/aircox_programs/admin.py @@ -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') diff --git a/aircox_programs/management/commands/sounds_monitor.py b/aircox_programs/management/commands/sounds_monitor.py index c513a94..976e01a 100644 --- a/aircox_programs/management/commands/sounds_monitor.py +++ b/aircox_programs/management/commands/sounds_monitor.py @@ -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) diff --git a/aircox_programs/management/commands/sounds_quality_check.py b/aircox_programs/management/commands/sounds_quality_check.py index 3273c14..ac8d0a6 100644 --- a/aircox_programs/management/commands/sounds_quality_check.py +++ b/aircox_programs/management/commands/sounds_quality_check.py @@ -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')) diff --git a/aircox_programs/models.py b/aircox_programs/models.py index a6d3b96..5747726 100755 --- a/aircox_programs/models.py +++ b/aircox_programs/models.py @@ -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 diff --git a/aircox_programs/utils.py b/aircox_programs/utils.py index 1311793..4f99e72 100644 --- a/aircox_programs/utils.py +++ b/aircox_programs/utils.py @@ -8,7 +8,7 @@ def to_timedelta (time): return datetime.timedelta( hours = time.hour, minutes = time.minute, - seconds = time.seconds + seconds = time.second )