diff --git a/aircox/liquidsoap/management/commands/liquidsoap.py b/aircox/liquidsoap/management/commands/liquidsoap.py index 110f17a..92881c9 100644 --- a/aircox/liquidsoap/management/commands/liquidsoap.py +++ b/aircox/liquidsoap/management/commands/liquidsoap.py @@ -1,15 +1,63 @@ """ -Monitor Liquidsoap's sources, logs, and even print what's on air. +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.core.management.base import BaseCommand, CommandError +from django.template.loader import render_to_string from django.utils import timezone as tz +import aircox.programs.models as models +import aircox.programs.settings as programs_settings + import aircox.liquidsoap.settings as settings import aircox.liquidsoap.utils as utils -import aircox.programs.models as models + + +class StationConfig: + controller = None + + def __init__ (self, station): + self.controller = utils.Controller(station, False) + + def handle (self, options): + if options.get('config') or options.get('all'): + self.make_config() + if options.get('streams') or options.get('all'): + self.make_playlists() + + def make_config (self): + context = { + 'controller': self.controller, + 'settings': settings, + } + + data = render_to_string('aircox_liquidsoap/station.liq', context) + data = re.sub(r'\s*\\\n', r'#\\n#', data) + data = data.replace('\n', '') + data = re.sub(r'#\\n#', '\n', data) + with open(self.controller.config_path, 'w+') as file: + file.write(data) + + def make_playlists (self): + for stream in self.controller.streams.values(): + program = stream.program + + sounds = models.Sound.objects.filter( + # good_quality = True, + type = models.Sound.Type['archive'], + path__startswith = os.path.join( + programs_settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, + program.path + ) + ) + with open(stream.path, 'w+') as file: + file.write('\n'.join(sound.path for sound in sounds)) class Command (BaseCommand): @@ -18,38 +66,69 @@ class Command (BaseCommand): def add_arguments (self, parser): parser.formatter_class=RawTextHelpFormatter - parser.add_argument( + + group = parser.add_argument_group('monitor') + group.add_argument( '-o', '--on_air', action='store_true', - help='Print what is on air' + help='print what is on air' ) - parser.add_argument( + group.add_argument( '-m', '--monitor', action='store_true', - help='Runs in monitor mode' + help='run in monitor mode' ) - parser.add_argument( + group.add_argument( '-d', '--delay', type=int, default=1000, - help='Time to sleep in milliseconds before update on monitor' + help='time to sleep in milliseconds before update on monitor' + ) + + group = parser.add_argument_group('configuration') + parser.add_argument( + '-s', '--station', type=int, + help='generate files for the given station (if not set, do it for' + ' all available stations)' + ) + parser.add_argument( + '-c', '--config', action='store_true', + help='generate liquidsoap config file' + ) + parser.add_argument( + '-S', '--streams', action='store_true', + help='generate all stream playlists' + ) + parser.add_argument( + '-a', '--all', action='store_true', + help='shortcut for -cS' ) def handle (self, *args, **options): - connector = utils.Connector() - self.monitor = utils.Monitor(connector) + if options.get('station'): + station = models.Station.objects.get(id = options.get('station')) + StationConfig(station).handle(options) + elif options.get('all') or options.get('config') or \ + options.get('streams'): + for station in models.Station.objects.filter(active = True): + StationConfig(station).handle(options) + + if options.get('on_air') or options.get('monitor'): + self.handle_monitor(options) + + def handle_monitor (self, options): + self.monitor = utils.Monitor() self.monitor.update() if options.get('on_air'): for id, controller in self.monitor.controller.items(): print(id, controller.on_air) + return if options.get('monitor'): delay = options.get('delay') / 1000 while True: for controller in self.monitor.controllers.values(): - try: - controller.monitor() - except Exception, e: - print(e) + controller.monitor() time.sleep(delay) + return diff --git a/aircox/liquidsoap/management/commands/liquidsoap_files.py b/aircox/liquidsoap/management/commands/liquidsoap_files.py deleted file mode 100644 index cd7674b..0000000 --- a/aircox/liquidsoap/management/commands/liquidsoap_files.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -Generate configuration files and playlists for liquidsoap using settings, streams and -so on -""" -import os -import re -from argparse import RawTextHelpFormatter - -from django.core.management.base import BaseCommand, CommandError -from django.views.generic.base import View -from django.template.loader import render_to_string - -import aircox.liquidsoap.settings as settings -import aircox.liquidsoap.utils as utils -import aircox.programs.settings as programs_settings -import aircox.programs.models as models - - -class Command (BaseCommand): - help= __doc__ - output_dir = settings.AIRCOX_LIQUIDSOAP_MEDIA - - def add_arguments (self, parser): - parser.formatter_class=RawTextHelpFormatter - parser.add_argument( - 'output', metavar='PATH', type=str, nargs='?', - help='force output to file (- to stdout) for single actions; to a ' - 'given dir when using --all') - parser.add_argument( - '-c', '--config', action='store_true', - help='Generate liquidsoap config file' - ) - parser.add_argument( - '-d', '--diffusion', action='store_true', - help='Generate the playlist for the current scheduled diffusion' - ) - parser.add_argument( - '-s', '--stream', type=int, - help='Generate the playlist of a stream with the given id' - ) - parser.add_argument( - '-S', '--streams', action='store_true', - help='Generate all stream playlists' - ) - parser.add_argument( - '-a', '--all', action='store_true', - help='Generate all playlists (stream and scheduled diffusion) ' - 'and config file' - ) - - def handle (self, *args, **options): - output = options.get('output') or None - if options.get('config'): - data = self.get_config(output = output) - return - - if options.get('stream'): - stream = options['stream'] - if type(stream) is int: - stream = models.Stream.objects.get(id = stream, - program__active = True) - - data = self.get_playlist(stream, output = output) - return - - if options.get('all') or options.get('streams'): - if output: - if not os.path.isdir(output): - raise CommandError('given output is not a directory') - self.output_dir = output - - if options.get('all'): - self.handle(config = True) - - for stream in models.Stream.objects.filter(program__active = True): - self.handle(stream = stream) - self.output_dir = settings.AIRCOX_LIQUIDSOAP_MEDIA - return - - raise CommandError('nothing to do') - - def print (self, data, path, default): - if path and path == '-': - print(data) - return - - if not path: - path = os.path.join(self.output_dir, default) - with open(path, 'w+') as file: - file.write(data) - - def get_config (self, output = None): - context = { - 'monitor': utils.Monitor(), - 'settings': settings, - } - - data = render_to_string('aircox_liquidsoap/config.liq', context) - data = re.sub(r'\s*\\\n', r'#\\n#', data) - data = data.replace('\n', '') - data = re.sub(r'#\\n#', '\n', data) - self.print(data, output, 'aircox.liq') - - def get_playlist (self, stream = None, output = None): - program = stream.program - source = utils.Source(program = program) - - sounds = models.Sound.objects.filter( - # good_quality = True, - type = models.Sound.Type['archive'], - path__startswith = os.path.join( - programs_settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, - program.path - ) - ) - data = '\n'.join(sound.path for sound in sounds) - self.print(data, output, utils.Source(program = program).path) - - diff --git a/aircox/liquidsoap/settings.py b/aircox/liquidsoap/settings.py index bfb881c..161e90f 100644 --- a/aircox/liquidsoap/settings.py +++ b/aircox/liquidsoap/settings.py @@ -5,8 +5,6 @@ def ensure (key, default): globals()[key] = getattr(settings, key, default) -ensure('AIRCOX_LIQUIDSOAP_SOCKET', '/tmp/liquidsoap.sock') - # dict of values to set (do not forget to escape chars) ensure('AIRCOX_LIQUIDSOAP_SET', { 'log.file.path': '"/tmp/liquidsoap.log"', @@ -18,7 +16,8 @@ ensure('AIRCOX_LIQUIDSOAP_SECURITY_SOURCE', '/media/data/musique/creation/Mega C # start the server on monitor if not present ensure('AIRCOX_LIQUIDSOAP_AUTOSTART', True) -# output directory for the generated files +# output directory for the generated files and socket. Each station has a subdir +# with the station's slug as name. ensure('AIRCOX_LIQUIDSOAP_MEDIA', '/tmp') diff --git a/aircox/liquidsoap/templates/aircox_liquidsoap/station.liq b/aircox/liquidsoap/templates/aircox_liquidsoap/station.liq index e222734..1351b3c 100644 --- a/aircox/liquidsoap/templates/aircox_liquidsoap/station.liq +++ b/aircox/liquidsoap/templates/aircox_liquidsoap/station.liq @@ -1,4 +1,7 @@ -{# Utilities #} +{# Context: #} +{# - controller: controller used to generate the current file #} +{# - settings: global settings #} + def interactive_source (id, s) = \ s = store_metadata(id=id, size=1, s) \ add_skip_command(s) \ @@ -13,13 +16,11 @@ end \ \ {# Config #} set("server.socket", true) \ -set("server.socket.path", "{{ settings.AIRCOX_LIQUIDSOAP_SOCKET }}") \ +set("server.socket.path", {{ controller.socket_path }}) \ {% for key, value in settings.AIRCOX_LIQUIDSOAP_SET.items %} set("{{ key|safe }}", {{ value|safe }}) \ {% endfor %} \ -\ -{% for controller in monitor.controllers.values %} {# station #} {{ controller.id }} = interactive_source ( \ "{{ controller.id }}", \ @@ -72,5 +73,4 @@ output.{{ output.get_type_display }}( \ {% endif %} ) \ {% endfor %} -{% endfor %} diff --git a/aircox/liquidsoap/utils.py b/aircox/liquidsoap/utils.py index a0a2b3f..45ae9f1 100644 --- a/aircox/liquidsoap/utils.py +++ b/aircox/liquidsoap/utils.py @@ -22,7 +22,7 @@ class Connector: """ __socket = None __available = False - address = settings.AIRCOX_LIQUIDSOAP_SOCKET + address = None @property def available (self): @@ -144,6 +144,7 @@ class Source: """ return os.path.join( settings.AIRCOX_LIQUIDSOAP_MEDIA, + self.station.slug, self.id + '.m3u' ) @@ -313,6 +314,9 @@ class Dealer (Source): class Controller: + """ + Main class controller for station and sources (streams and dealer) + """ connector = None station = None # the related station master = None # master source (station's source) @@ -331,15 +335,45 @@ class Controller: def name (self): return self.master and self.master.name - def __init__ (self, station, connector = None): + @property + def path (self): """ - use_connector: avoids the creation of a Connector, in case it is not needed + Directory path where all station's related files are put. + """ + return os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA, + self.station.slug) + + @property + def socket_path (self): + """ + Connector's socket path + """ + return os.path.join(self.path, 'station.sock') + + @property + def config_path (self): + """ + Connector's socket path + """ + return os.path.join(self.path, 'station.liq') + + def __init__ (self, station, connector = True): + """ + Params: + - station: managed station + - connector: if true, create a connector, else do not + + Initialize a master, a dealer and all streams that are connected + to the given station; We ensure the existence of the controller's + files dir. """ - self.connector = connector self.station = station self.station.controller = self self.outputs = models.Output.objects.filter(station = station) + os.makedirs(self.path, exist_ok = True) + self.connector = connector and Connector(self.socket_path) + self.master = Master(self) self.dealer = Dealer(self) self.streams = { @@ -419,11 +453,11 @@ class Monitor: """ controllers = None - def __init__ (self, connector = None): + def __init__ (self): self.controllers = { controller.id : controller for controller in [ - Controller(station, connector) + Controller(station, True) for station in programs.Station.objects.filter(active = True) ] } diff --git a/aircox/liquidsoap/views.py b/aircox/liquidsoap/views.py index cabcfcd..3dea9d4 100644 --- a/aircox/liquidsoap/views.py +++ b/aircox/liquidsoap/views.py @@ -14,11 +14,8 @@ view_monitor = None def get_monitor(): global view_monitor - if not view_monitor: - view_monitor = utils.Monitor( - utils.Connector(address = settings.AIRCOX_LIQUIDSOAP_SOCKET) - ) + view_monitor = utils.Monitor() return view_monitor class Actions: