make the controllers' manager; fix errors, make it working
This commit is contained in:
		@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
"""
 | 
			
		||||
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
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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.
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user