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):
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)

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
"""
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
)

View File

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

View File

@ -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:

View File

@ -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
try:
self.current_source = next(
source for source in self.station.get_sources()
if source.rid == rid
][0]
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

View File

@ -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.

View File

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

View File

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

View File

@ -356,12 +356,11 @@ class Logs(ListByDate):
track = log.related
post = ListItem(
title = '{artist} &#8212; {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