make the controllers' manager; fix errors, make it working

This commit is contained in:
bkfox 2016-07-19 18:32:07 +02:00
parent 5a77b4d4ea
commit f87c660878
10 changed files with 191 additions and 41 deletions

View File

@ -16,14 +16,13 @@ class OutputInline(admin.StackedInline):
class StationAdmin(admin.ModelAdmin): class StationAdmin(admin.ModelAdmin):
inlines = [ SourceInline, OutputInline ] inlines = [ SourceInline, OutputInline ]
#@admin.register(Log) @admin.register(models.Log)
#class LogAdmin(admin.ModelAdmin): class LogAdmin(admin.ModelAdmin):
# list_display = ['id', 'date', 'source', 'comment', 'related_object'] list_display = ['id', 'date', 'station', 'source', 'comment', 'related']
# list_filter = ['date', 'source', 'related_type'] list_filter = ['date', 'source', 'related_type']
admin.site.register(models.Source) admin.site.register(models.Source)
admin.site.register(models.Output) admin.site.register(models.Output)
admin.site.register(models.Log)

View File

@ -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()

View File

@ -12,6 +12,7 @@ sources that are used to generate the audio stream:
- **master**: main output - **master**: main output
""" """
import os import os
import logging
from enum import Enum, IntEnum from enum import Enum, IntEnum
from django.db import models from django.db import models
@ -24,6 +25,9 @@ from aircox.programs.utils import to_timedelta
import aircox.controllers.settings as settings import aircox.controllers.settings as settings
from aircox.controllers.plugins.plugins import Plugins from aircox.controllers.plugins.plugins import Plugins
logger = logging.getLogger('aircox.controllers')
Plugins.discover() Plugins.discover()
@ -142,7 +146,7 @@ class Station(programs.Nameable):
the queryset; the queryset;
""" """
qs = Log.get_for(model = models) \ 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: if not archives and self.dealer:
qs = qs.exclude( qs = qs.exclude(
source = self.dealer.id_, source = self.dealer.id_,
@ -270,7 +274,7 @@ class Source(programs.Nameable):
self.controller.playlist = diffusion.playlist self.controller.playlist = diffusion.playlist
return return
program = program or self.stream program = program or self.program
if program: if program:
self.controller.playlist = [ sound.path for sound in self.controller.playlist = [ sound.path for sound in
programs.Sound.objects.filter( programs.Sound.objects.filter(
@ -404,12 +408,12 @@ class Log(programs.Related):
str(self), str(self),
self.comment or '', self.comment or '',
' -- {} #{}'.format(self.related_type, self.related_id) ' -- {} #{}'.format(self.related_type, self.related_id)
if self.related_object else '' if self.related else ''
) )
def __str__(self): def __str__(self):
return '#{} ({}, {})'.format( 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
) )

View File

@ -1,7 +1,9 @@
import time
from django.utils import timezone as tz from django.utils import timezone as tz
import aircox.programs.models as programs import aircox.programs.models as programs
from aircox.controller.models import Log from aircox.controllers.models import Log
class Monitor: class Monitor:
""" """
@ -19,22 +21,28 @@ class Monitor:
station = None station = None
controller = 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 Run all monitoring functions. Ensure that station has controllers
""" """
if not self.controller: if not self.controller:
self.station.prepare() self.station.prepare()
for stream in self.station.stream_sources:
stream.load_playlist()
self.controller = self.station.controller self.controller = self.station.controller
self.trace() self.trace()
self.handler() self.handle()
def log(self, **kwargs): def log(self, **kwargs):
""" """
Create a log using **kwargs, and print info Create a log using **kwargs, and print info
""" """
log = programs.Log(station = self.station, **kwargs) log = Log(station = self.station, **kwargs)
log.save() log.save()
log.print() log.print()
@ -46,7 +54,7 @@ class Monitor:
self.controller.fetch() self.controller.fetch()
current_sound = self.controller.current_sound current_sound = self.controller.current_sound
current_source = self.controller.current_source current_source = self.controller.current_source
if not current_sound: if not current_sound or not current_source:
return return
log = Log.get_for(model = programs.Sound) \ log = Log.get_for(model = programs.Sound) \
@ -61,7 +69,7 @@ class Monitor:
log.related.path == current_sound): log.related.path == current_sound):
return return
sound = programs.Sound.object.filter(path = current_sound) sound = programs.Sound.objects.filter(path = current_sound)
self.log( self.log(
type = Log.Type.play, type = Log.Type.play,
source = current_source.id_, source = current_source.id_,
@ -77,17 +85,17 @@ class Monitor:
""" """
logs = Log.get_for(model = programs.Track) \ logs = Log.get_for(model = programs.Track) \
.filter(pk__gt = log.pk) .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) tracks = programs.Track.get_for(object = log.related) \
.filter(pos_in_sec = True) .filter(pos_in_secs = True)
if len(tracks) == len(logs): if tracks and len(tracks) == len(logs):
return return
tracks = tracks.exclude(pk__in = logs).order_by('pos') tracks = tracks.exclude(pk__in = logs).order_by('position')
now = tz.now() now = tz.now()
for track in tracks: for track in tracks:
pos = log.date + tz.timedelta(seconds = track.pos) pos = log.date + tz.timedelta(seconds = track.position)
if pos < now: if pos < now:
self.log( self.log(
type = Log.Type.play, type = Log.Type.play,
@ -165,8 +173,8 @@ class Monitor:
playlist += next_playlist playlist += next_playlist
# playlist update # playlist update
if dealer.playlist != playlist: if dealer.controller.playlist != playlist:
dealer.playlist = playlist dealer.controller.playlist = playlist
if next_diff: if next_diff:
self.log( self.log(
type = Log.Type.load, type = Log.Type.load,
@ -187,3 +195,4 @@ class Monitor:
related_object = next_diff, related_object = next_diff,
) )

View File

@ -56,7 +56,6 @@ class Connector:
if data: if data:
data = reg.sub(r'\1', data) data = reg.sub(r'\1', data)
data = data.strip() data = data.strip()
if parse: if parse:
data = self.parse(data) data = self.parse(data)
elif parse_json: elif parse_json:

View File

@ -1,4 +1,6 @@
import os import os
import subprocess
import atexit
import aircox.controllers.plugins.plugins as plugins import aircox.controllers.plugins.plugins as plugins
from aircox.controllers.plugins.connector import Connector from aircox.controllers.plugins.connector import Connector
@ -29,7 +31,10 @@ class StationController(plugins.StationController):
self.connector = Connector(self.socket_path) self.connector = Connector(self.socket_path)
def _send(self, *args, **kwargs): 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): def fetch(self):
super().fetch() super().fetch()
@ -43,14 +48,17 @@ class StationController(plugins.StationController):
return return
self.current_sound = data.get('initial_uri') self.current_sound = data.get('initial_uri')
self.current_source = [ try:
# we assume sound is always from a registered source self.current_source = next(
source for source in self.station.get_sources() source for source in self.station.get_sources()
if source.rid == rid if source.controller.rid == rid
][0] )
except:
self.current_source = None
class SourceController(plugins.SourceController): class SourceController(plugins.SourceController):
rid = None
connector = None connector = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -58,7 +66,7 @@ class SourceController(plugins.SourceController):
self.connector = self.source.station.controller.connector self.connector = self.source.station.controller.connector
def _send(self, *args, **kwargs): def _send(self, *args, **kwargs):
self.connector.send(*args, **kwargs) return self.connector.send(*args, **kwargs)
@property @property
def active(self): def active(self):
@ -76,7 +84,7 @@ class SourceController(plugins.SourceController):
self._send(self.source.slug, '.skip') self._send(self.source.slug, '.skip')
def fetch(self): def fetch(self):
data = self._send(self.source.slug, '.get', parse = True) data = self._send(self.source.id_, '.get', parse = True)
if not data: if not data:
return return

View File

@ -1,5 +1,7 @@
import os import os
import re import re
import subprocess
import atexit
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -57,8 +59,7 @@ class StationController:
""" """
Current source object that is responsible of self.current_sound Current source object that is responsible of self.current_sound
""" """
process = None
# TODO: add function to launch external program?
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@ -76,6 +77,46 @@ class StationController:
if source.controller: if source.controller:
source.controller.fetch() 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): def push(self, config = True):
""" """
Update configuration and children's info. Update configuration and children's info.

View File

@ -2,6 +2,7 @@ import copy
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline
from django.db import models from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
@ -44,7 +45,6 @@ class DiffusionInline(admin.StackedInline):
# sortable = 'position' # sortable = 'position'
# extra = 10 # extra = 10
class NameableAdmin(admin.ModelAdmin): class NameableAdmin(admin.ModelAdmin):
fields = [ 'name' ] fields = [ 'name' ]
@ -53,6 +53,15 @@ class NameableAdmin(admin.ModelAdmin):
search_fields = ['name',] 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) @admin.register(Sound)
class SoundAdmin(NameableAdmin): class SoundAdmin(NameableAdmin):
fields = None fields = None
@ -64,6 +73,7 @@ class SoundAdmin(NameableAdmin):
(None, { 'fields': ['removed', 'good_quality' ] } ) (None, { 'fields': ['removed', 'good_quality' ] } )
] ]
readonly_fields = ('path', 'duration',) readonly_fields = ('path', 'duration',)
inlines = [TrackInline]
@admin.register(Stream) @admin.register(Stream)

View File

@ -127,7 +127,7 @@ class SoundInfo:
if not os.path.exists(path): if not os.path.exists(path):
return return
old = Tracks.get_for(object = sound).exclude(tracks_id) old = Track.get_for(object = sound)
if old: if old:
return return
@ -296,6 +296,7 @@ class Command(BaseCommand):
# sounds in directory # sounds in directory
for path in os.listdir(subdir): for path in os.listdir(subdir):
print(path)
path = os.path.join(subdir, path) path = os.path.join(subdir, path)
if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT): if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT):
continue continue

View File

@ -356,12 +356,11 @@ class Logs(ListByDate):
track = log.related track = log.related
post = ListItem( post = ListItem(
title = '{artist} &#8212; {name}'.format( title = track.name,
artist = track.artist, subtitle = track.artist,
name = track.name,
),
date = log.date, date = log.date,
content = track.info, content = track.info,
css_class = 'track',
info = '', info = '',
) )
return post return post