diff --git a/controllers/admin.py b/controllers/admin.py index c8bff2e..6521d70 100644 --- a/controllers/admin.py +++ b/controllers/admin.py @@ -16,14 +16,13 @@ class OutputInline(admin.StackedInline): class StationAdmin(admin.ModelAdmin): inlines = [ SourceInline, OutputInline ] -#@admin.register(Log) -#class LogAdmin(admin.ModelAdmin): -# list_display = ['id', 'date', 'source', 'comment', 'related_object'] -# list_filter = ['date', 'source', 'related_type'] +@admin.register(models.Log) +class LogAdmin(admin.ModelAdmin): + list_display = ['id', 'date', 'station', 'source', 'comment', 'related'] + list_filter = ['date', 'source', 'related_type'] admin.site.register(models.Source) admin.site.register(models.Output) -admin.site.register(models.Log) diff --git a/controllers/management/commands/controllers.py b/controllers/management/commands/controllers.py new file mode 100644 index 0000000..733a87f --- /dev/null +++ b/controllers/management/commands/controllers.py @@ -0,0 +1,80 @@ +""" +Main tool to work with liquidsoap. We can: +- monitor Liquidsoap's sources and do logs, print what's on air. +- generate configuration files and playlists for a given station +""" +import os +import time +import re + +from argparse import RawTextHelpFormatter + +from django.conf import settings as main_settings +from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone as tz + +import aircox.programs.models as programs +from aircox.controllers.models import Log, Station +from aircox.controllers.monitor import Monitor + + +class Command (BaseCommand): + help= __doc__ + + def add_arguments (self, parser): + parser.formatter_class=RawTextHelpFormatter + group = parser.add_argument_group('actions') + group.add_argument( + '-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' + ) + group.add_argument( + '-r', '--run', action='store_true', + help='run the required applications for the stations' + ) + + group = parser.add_argument_group('options') + group.add_argument( + '-s', '--station', type=str, action='append', + help='name of the station to monitor instead of monitoring ' + 'all stations' + ) + group.add_argument( + '-d', '--delay', type=int, + default=1000, + help='time to sleep in milliseconds between two updates when we ' + 'monitor' + ) + + def handle (self, *args, + config = None, run = None, monitor = None, + station = [], delay = 1000, + **options): + + stations = Station.objects.filter(name__in = station)[:] \ + if station else \ + Station.objects.all()[:] + + for station in stations: + station.prepare() + if config and not run: # no need to write it twice + station.controller.push() + if run: + station.controller.process_run() + + if monitor: + monitors = [ Monitor(station) for station in stations ] + delay = delay / 1000 + while True: + for monitor in monitors: + monitor.monitor() + time.sleep(delay) + + if run: + for station in stations: + station.controller.process_wait() + diff --git a/controllers/models.py b/controllers/models.py index e88f1aa..fb6fc7a 100644 --- a/controllers/models.py +++ b/controllers/models.py @@ -12,6 +12,7 @@ sources that are used to generate the audio stream: - **master**: main output """ import os +import logging from enum import Enum, IntEnum from django.db import models @@ -24,6 +25,9 @@ from aircox.programs.utils import to_timedelta import aircox.controllers.settings as settings from aircox.controllers.plugins.plugins import Plugins + +logger = logging.getLogger('aircox.controllers') + Plugins.discover() @@ -142,7 +146,7 @@ class Station(programs.Nameable): the queryset; """ qs = Log.get_for(model = models) \ - .filter(station = station, type = Log.Type.play) + .filter(station = self, type = Log.Type.play) if not archives and self.dealer: qs = qs.exclude( source = self.dealer.id_, @@ -270,7 +274,7 @@ class Source(programs.Nameable): self.controller.playlist = diffusion.playlist return - program = program or self.stream + program = program or self.program if program: self.controller.playlist = [ sound.path for sound in programs.Sound.objects.filter( @@ -404,12 +408,12 @@ class Log(programs.Related): str(self), self.comment or '', ' -- {} #{}'.format(self.related_type, self.related_id) - if self.related_object else '' + if self.related else '' ) def __str__(self): return '#{} ({}, {})'.format( - self.pk, self.date.strftime('%Y/%m/%d %H:%M'), self.source.name + self.pk, self.date.strftime('%Y/%m/%d %H:%M'), self.source ) diff --git a/controllers/monitor.py b/controllers/monitor.py index bccf4cb..c103944 100644 --- a/controllers/monitor.py +++ b/controllers/monitor.py @@ -1,7 +1,9 @@ +import time + from django.utils import timezone as tz import aircox.programs.models as programs -from aircox.controller.models import Log +from aircox.controllers.models import Log class Monitor: """ @@ -19,22 +21,28 @@ class Monitor: station = None controller = None - def run(self): + def __init__(self, station): + self.station = station + + def monitor(self): """ Run all monitoring functions. Ensure that station has controllers """ if not self.controller: self.station.prepare() + for stream in self.station.stream_sources: + stream.load_playlist() + self.controller = self.station.controller self.trace() - self.handler() + self.handle() def log(self, **kwargs): """ Create a log using **kwargs, and print info """ - log = programs.Log(station = self.station, **kwargs) + log = Log(station = self.station, **kwargs) log.save() log.print() @@ -46,7 +54,7 @@ class Monitor: self.controller.fetch() current_sound = self.controller.current_sound current_source = self.controller.current_source - if not current_sound: + if not current_sound or not current_source: return log = Log.get_for(model = programs.Sound) \ @@ -61,7 +69,7 @@ class Monitor: log.related.path == current_sound): return - sound = programs.Sound.object.filter(path = current_sound) + sound = programs.Sound.objects.filter(path = current_sound) self.log( type = Log.Type.play, source = current_source.id_, @@ -77,17 +85,17 @@ class Monitor: """ logs = Log.get_for(model = programs.Track) \ .filter(pk__gt = log.pk) - logs = [ log.pk for log in logs ] + logs = [ log.related_id for log in logs ] - tracks = programs.Track.get_for(object = log.related) - .filter(pos_in_sec = True) - if len(tracks) == len(logs): + tracks = programs.Track.get_for(object = log.related) \ + .filter(pos_in_secs = True) + if tracks and len(tracks) == len(logs): return - tracks = tracks.exclude(pk__in = logs).order_by('pos') + tracks = tracks.exclude(pk__in = logs).order_by('position') now = tz.now() for track in tracks: - pos = log.date + tz.timedelta(seconds = track.pos) + pos = log.date + tz.timedelta(seconds = track.position) if pos < now: self.log( type = Log.Type.play, @@ -165,8 +173,8 @@ class Monitor: playlist += next_playlist # playlist update - if dealer.playlist != playlist: - dealer.playlist = playlist + if dealer.controller.playlist != playlist: + dealer.controller.playlist = playlist if next_diff: self.log( type = Log.Type.load, @@ -187,3 +195,4 @@ class Monitor: related_object = next_diff, ) + diff --git a/controllers/plugins/connector.py b/controllers/plugins/connector.py index 495de89..047fc65 100644 --- a/controllers/plugins/connector.py +++ b/controllers/plugins/connector.py @@ -56,7 +56,6 @@ class Connector: if data: data = reg.sub(r'\1', data) data = data.strip() - if parse: data = self.parse(data) elif parse_json: diff --git a/controllers/plugins/liquidsoap.py b/controllers/plugins/liquidsoap.py index 4947ebf..9c0a90c 100644 --- a/controllers/plugins/liquidsoap.py +++ b/controllers/plugins/liquidsoap.py @@ -1,4 +1,6 @@ import os +import subprocess +import atexit import aircox.controllers.plugins.plugins as plugins from aircox.controllers.plugins.connector import Connector @@ -29,7 +31,10 @@ class StationController(plugins.StationController): self.connector = Connector(self.socket_path) def _send(self, *args, **kwargs): - self.connector.send(*args, **kwargs) + return self.connector.send(*args, **kwargs) + + def __get_process_args(self): + return ['liquidsoap', '-v', self.path] def fetch(self): super().fetch() @@ -38,19 +43,22 @@ class StationController(plugins.StationController): if not rid: return - data = self._send('request.metadata', rid, parse = True) + data = self._send('request.metadata ', rid, parse = True) if not data: return self.current_sound = data.get('initial_uri') - self.current_source = [ - # we assume sound is always from a registered source - source for source in self.station.get_sources() - if source.rid == rid - ][0] + try: + self.current_source = next( + source for source in self.station.get_sources() + if source.controller.rid == rid + ) + except: + self.current_source = None class SourceController(plugins.SourceController): + rid = None connector = None def __init__(self, *args, **kwargs): @@ -58,7 +66,7 @@ class SourceController(plugins.SourceController): self.connector = self.source.station.controller.connector def _send(self, *args, **kwargs): - self.connector.send(*args, **kwargs) + return self.connector.send(*args, **kwargs) @property def active(self): @@ -76,7 +84,7 @@ class SourceController(plugins.SourceController): self._send(self.source.slug, '.skip') def fetch(self): - data = self._send(self.source.slug, '.get', parse = True) + data = self._send(self.source.id_, '.get', parse = True) if not data: return diff --git a/controllers/plugins/plugins.py b/controllers/plugins/plugins.py index 6379ab1..d0f3e3d 100644 --- a/controllers/plugins/plugins.py +++ b/controllers/plugins/plugins.py @@ -1,5 +1,7 @@ import os import re +import subprocess +import atexit from django.template.loader import render_to_string @@ -57,8 +59,7 @@ class StationController: """ Current source object that is responsible of self.current_sound """ - - # TODO: add function to launch external program? + process = None def __init__(self, **kwargs): self.__dict__.update(kwargs) @@ -76,6 +77,46 @@ class StationController: if source.controller: source.controller.fetch() + def __get_process_args(self): + """ + Get arguments for the executed application. Called by exec, to be + used as subprocess.Popen(__get_process_args()). + If no value is returned, abort the execution. + + Must be implemented by the plugin + """ + return [] + + def process_run(self): + """ + Execute the external application with corresponding informations. + + This function must make sure that all needed files have been generated. + """ + if self.process: + return + + self.push() + + args = self.__get_process_args() + if not args: + return + self.process = subprocess.Popen(args, stderr=subprocess.STDOUT) + atexit.register(self.process.terminate) + + def process_terminate(self): + if self.process: + self.process.terminate() + self.process = None + + def process_wait(self): + """ + Wait for the process to terminate if there is a process + """ + if self.process: + self.process.wait() + self.process = None + def push(self, config = True): """ Update configuration and children's info. diff --git a/programs/admin.py b/programs/admin.py index 057d847..127b928 100755 --- a/programs/admin.py +++ b/programs/admin.py @@ -2,6 +2,7 @@ import copy from django import forms from django.contrib import admin +from django.contrib.contenttypes.admin import GenericTabularInline from django.db import models from django.utils.translation import ugettext as _, ugettext_lazy @@ -44,7 +45,6 @@ class DiffusionInline(admin.StackedInline): # sortable = 'position' # extra = 10 - class NameableAdmin(admin.ModelAdmin): fields = [ 'name' ] @@ -53,6 +53,15 @@ class NameableAdmin(admin.ModelAdmin): search_fields = ['name',] +class TrackInline(GenericTabularInline): + ct_field = 'related_type' + ct_fk_field = 'related_id' + model = Track + extra = 0 + fields = ('artist', 'title', 'tags', 'info', 'position') + readonly_fields = ('position',) + + @admin.register(Sound) class SoundAdmin(NameableAdmin): fields = None @@ -64,6 +73,7 @@ class SoundAdmin(NameableAdmin): (None, { 'fields': ['removed', 'good_quality' ] } ) ] readonly_fields = ('path', 'duration',) + inlines = [TrackInline] @admin.register(Stream) diff --git a/programs/management/commands/sounds_monitor.py b/programs/management/commands/sounds_monitor.py index 1b16a9a..6661b02 100644 --- a/programs/management/commands/sounds_monitor.py +++ b/programs/management/commands/sounds_monitor.py @@ -127,7 +127,7 @@ class SoundInfo: if not os.path.exists(path): return - old = Tracks.get_for(object = sound).exclude(tracks_id) + old = Track.get_for(object = sound) if old: return @@ -296,6 +296,7 @@ class Command(BaseCommand): # sounds in directory for path in os.listdir(subdir): + print(path) path = os.path.join(subdir, path) if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT): continue diff --git a/website/sections.py b/website/sections.py index aa3e6e7..3a1a1a0 100644 --- a/website/sections.py +++ b/website/sections.py @@ -356,12 +356,11 @@ class Logs(ListByDate): track = log.related post = ListItem( - title = '{artist} — {name}'.format( - artist = track.artist, - name = track.name, - ), + title = track.name, + subtitle = track.artist, date = log.date, content = track.info, + css_class = 'track', info = '♫', ) return post