make the controllers' manager; fix errors, make it working
This commit is contained in:
parent
5a77b4d4ea
commit
f87c660878
|
@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
80
controllers/management/commands/controllers.py
Normal file
80
controllers/management/commands/controllers.py
Normal 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()
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -356,12 +356,11 @@ class Logs(ListByDate):
|
||||||
|
|
||||||
track = log.related
|
track = log.related
|
||||||
post = ListItem(
|
post = ListItem(
|
||||||
title = '{artist} — {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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user