From 29d0929a0c26881f88e06a24f69712a4a933652a Mon Sep 17 00:00:00 2001 From: bkfox Date: Mon, 16 May 2016 18:43:36 +0200 Subject: [PATCH] fix errors, update a bit how liquidsoap part work and so on --- liquidsoap/management/commands/liquidsoap.py | 159 +++----- .../templates/aircox/liquidsoap/station.liq | 16 +- liquidsoap/utils.py | 360 ++++++++++-------- .../__pycache__/__init__.cpython-34.pyc | Bin 147 -> 141 bytes .../management/commands/sounds_monitor.py | 6 +- 5 files changed, 264 insertions(+), 277 deletions(-) diff --git a/liquidsoap/management/commands/liquidsoap.py b/liquidsoap/management/commands/liquidsoap.py index b9a2f50..fc27ffc 100644 --- a/liquidsoap/management/commands/liquidsoap.py +++ b/liquidsoap/management/commands/liquidsoap.py @@ -23,70 +23,6 @@ import aircox.liquidsoap.settings as settings import aircox.liquidsoap.utils as utils -class StationConfig: - """ - Configuration and playlist generator for a station. - """ - controller = None - process = None - - def __init__ (self, station): - self.controller = utils.Controller(station, False) - - def handle (self, options): - os.makedirs(self.controller.path, exist_ok = True) - 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): - log_script = main_settings.BASE_DIR \ - if hasattr(main_settings, 'BASE_DIR') else \ - main_settings.PROJECT_ROOT - log_script = os.path.join(log_script, 'manage.py') + \ - ' liquidsoap_log' - - context = { - 'controller': self.controller, - 'settings': settings, - 'log_script': log_script, - } - - 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 = programs.Sound.objects.filter( - # good_quality = True, - type = programs.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)) - - def run (self): - """ - Run subprocess in background, register a terminate handler, and - return process instance. - """ - self.process = \ - subprocess.Popen(['liquidsoap', '-v', self.controller.config_path], - stderr=subprocess.STDOUT) - atexit.register(self.process.terminate) - return self.process - - class Monitor: @classmethod def run (cl, controller): @@ -128,6 +64,7 @@ class Monitor: diffusion.playlist = [ sound.path for sound in diffusion.get_archives() ] + diffusion.playlist.save() if diffusion.playlist and on_air not in diffusion.playlist: return diffusion @@ -152,6 +89,7 @@ class Monitor: on_air not in playlist: dealer.on = False dealer.playlist = diff.playlist + dealer.playlist.save() # run the diff if dealer.playlist == diff.playlist and diff.start <= now and not dealer.on: @@ -214,70 +152,79 @@ class Command (BaseCommand): help='run liquidsoap on exit' ) - group = parser.add_argument_group('monitor') + group = parser.add_argument_group('actions') group.add_argument( - '-o', '--on_air', action='store_true', - help='print what is on air' + '-d', '--delay', type=int, + default=1000, + help='time to sleep in milliseconds between two updates when we ' + 'monitor' ) group.add_argument( '-m', '--monitor', action='store_true', help='run in monitor mode' ) group.add_argument( - '-d', '--delay', type=int, - default=1000, - help='time to sleep in milliseconds before update on monitor' - ) - - group = parser.add_argument_group('configuration') - group.add_argument( - '-s', '--station', type=int, - help='generate files for the given station' - ) - group.add_argument( - '-a', '--all', action='store_true', - help='generate files for all stations' - ) - group.add_argument( - '-c', '--config', action='store_true', - help='generate liquidsoap config file' - ) - group.add_argument( - '-S', '--streams', action='store_true', - help='generate all stream playlists' + '-o', '--on_air', action='store_true', + help='print what is on air' ) group.add_argument( '-r', '--run', action='store_true', help='run liquidsoap with the generated configuration' ) + group.add_argument( + '-w', '--write', action='store_true', + help='write configuration and playlist' + ) + + group = parser.add_argument_group('selector') + group.add_argument( + '-s', '--station', type=int, action='append', + help='select station(s) with this id' + ) + group.add_argument( + '-a', '--all', action='store_true', + help='select all stations' + ) def handle (self, *args, **options): + # selector stations = [] - if options.get('station'): - stations = [ StationConfig( - programs.Station.objects.get( - id = options.get('station') - )) ] - elif options.get('all') or options.get('config') or \ - options.get('streams'): - stations = [ StationConfig(station) - for station in \ - programs.Station.objects.filter(active = True) - ] + if options.get('all'): + stations = programs.Station.objects.filter(active = True) + elif options.get('station'): + stations = programs.Station.objects.filter( + id__in = options.get('station') + ) run = options.get('run') - for station in stations: - station.handle(options) - if run: - station.run() + monitor = options.get('on_air') or options.get('monitor') - if options.get('on_air') or options.get('monitor'): + self.controllers = [ utils.Controller(station, connector = monitor) + for station in stations ] + + # actions + if options.get('write') or run: + self.handle_write() + if run: + self.handle_run() + if monitor: self.handle_monitor(options) + # post if run: - for station in stations: - station.process.wait() + for controller in self.controllers: + controller.process.wait() + def handle_write (self): + for controller in self.controllers: + controller.write_data() + + def handle_run (self): + for controller in self.controllers: + controller.process = \ + subprocess.Popen(['liquidsoap', '-v', controller.config_path], + stderr=subprocess.STDOUT) + atexit.register(controller.process.terminate) def handle_monitor (self, options): controllers = [ diff --git a/liquidsoap/templates/aircox/liquidsoap/station.liq b/liquidsoap/templates/aircox/liquidsoap/station.liq index 820ae4d..011c4e0 100644 --- a/liquidsoap/templates/aircox/liquidsoap/station.liq +++ b/liquidsoap/templates/aircox/liquidsoap/station.liq @@ -31,7 +31,7 @@ set("{{ key|safe }}", {{ value|safe }}) \ at(interactive.bool('{{ source.id }}_on', false), \ interactive_source('{{ source.id }}', playlist.once( \ reload_mode='watch', \ - "{{ source.path }}", \ + "{{ source.playlist.path }}", \ )) \ ), \ {% endif %} @@ -41,17 +41,21 @@ set("{{ key|safe }}", {{ value|safe }}) \ interactive_source("{{ controller.id }}_streams", rotate([ \ {% for source in controller.streams.values %} {% with info=source.stream_info %} - {% if info.delay %} - delay({{ info.delay }}., stream("{{ source.id }}", "{{ source.path }}")), \ - {% elif info.begin and info.end %} - at({ {{info.begin}}-{{info.end}} }, stream("{{ source.id }}", "{{ source.path }}")), \ - {% endif %} + {% with path=source.playlist.path %} + {% if info.delay %} + delay({{ info.delay }}., stream("{{ source.id }}", "{{ path }}")), \ + {% elif info.begin and info.end %} + at({ {{info.begin}}-{{info.end}} }, stream("{{ source.id }}", "{{ path }}")), \ + {% endif %} + {% endwith %} {% endwith %} {% endfor %} {% for source in controller.streams.values %} {% if not source.stream_info %} + {% with path=source.playlist.path %} stream("{{ source.id }}", "{{ source.path }}"), \ + {% endwith %} {% endif %} {% endfor %} ])), \ diff --git a/liquidsoap/utils.py b/liquidsoap/utils.py index e9349d5..70a5e7e 100644 --- a/liquidsoap/utils.py +++ b/liquidsoap/utils.py @@ -2,11 +2,15 @@ import os import socket import re import json +import subprocess from django.utils.translation import ugettext as _, ugettext_lazy from django.utils import timezone as tz +from django.conf import settings as main_settings +from django.template.loader import render_to_string import aircox.programs.models as programs +import aircox.programs.settings as programs_settings import aircox.liquidsoap.models as models import aircox.liquidsoap.settings as settings @@ -23,14 +27,14 @@ class Connector: address = None @property - def available (self): + def available(self): return self.__available - def __init__ (self, address = None): + def __init__(self, address = None): if address: self.address = address - def open (self): + def open(self): if self.__available: return @@ -45,7 +49,7 @@ class Connector: self.__available = False return -1 - def send (self, *data, try_count = 1, parse = False, parse_json = False): + def send(self, *data, try_count = 1, parse = False, parse_json = False): if self.open(): return '' data = bytes(''.join([str(d) for d in data]) + '\n', encoding='utf-8') @@ -71,7 +75,7 @@ class Connector: if try_count > 0: return self.send(data, try_count - 1) - def parse (self, string): + def parse(self, string): string = string.split('\n') data = {} for line in string: @@ -82,7 +86,7 @@ class Connector: data[line['key']] = line['value'] return data - def parse_json (self, string): + def parse_json(self, string): try: if string[0] == '"' and string[-1] == '"': string = string[1:-1] @@ -91,85 +95,142 @@ class Connector: return None -class Source: - """ - A structure that holds informations about a LiquidSoap source. - """ +class Playlist(list): + path = None + + def __init__(self, path = None, items = None, program = None): + self.path = path + self.program = program + if program: + self.load_from_db() + elif path: + self.load() + elif items: + self.extend(items) + + def save(self): + """ + Save data to the playlist file + """ + os.makedirs(os.path.dirname(self.path), exist_ok = True) + with open(self.path, 'w') as file: + file.write('\n'.join(self)) + + def load(self): + """ + Load data from playlist file + """ + if not os.path.exists(self.path): + return + with open(self.path, 'r') as file: + self.clear() + self.extend(file.readlines()) + + def load_from_db(self, clear = True): + """ + Update content from the database using the given program + If clear is True, clear older items, otherwise append to the + current playlist. + If save is True, save the playlist to the playlist file + """ + sounds = programs.Sound.objects.filter( + type = programs.Sound.Type['archive'], + path__startswith = os.path.join( + programs_settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, + self.program.path + ), + # good_quality = True + removed = False + ) + self.clear() + self.extend([sound.path for sound in sounds]) + +class BaseSource: + id = None + name = None controller = None - program = None metadata = None - def __init__ (self, controller = None, program = None): + def __init__(self, controller, id, name): + self.id = id + self.name = name self.controller = controller - self.program = program + + def _send(self, *args, **kwargs): + self.controller.connector.send(*args, **kwargs) @property - def station (self): - """ - Proxy to self.(program|controller).station - """ - return self.program.station if self.program else \ - self.controller.station - - @property - def connector (self): - """ - Proxy to self.controller.connector - """ - return self.controller.connector - - @property - def id (self): - """ - Identifier for the source, scoped in the station's one - """ - postfix = ('_stream_' + str(self.program.id)) if self.program else '' - return self.station.slug + postfix - - @property - def name (self): - """ - Name of the related object (program or station) - """ - if self.program: - return self.program.name - return self.station.name - - @property - def path (self): - """ - Path to the playlist - """ - return os.path.join( - settings.AIRCOX_LIQUIDSOAP_MEDIA, - self.station.slug, - self.id + '.m3u' - ) - - @property - def playlist (self): - """ - Get or set the playlist as an array, and update it into - the corresponding file. - """ - try: - with open(self.path, 'r') as file: - return file.readlines() - except: - return [] - - @playlist.setter - def playlist (self, sounds): - with open(self.path, 'w') as file: - file.write('\n'.join(sounds)) - - - @property - def current_sound (self): + def current_sound(self): self.update() return self.metadata.get('initial_uri') if self.metadata else {} - def stream_info (self): + def skip(self): + """ + Skip a given source. If no source, use master. + """ + self._send(self.id, '.skip') + + def update(self, metadata = None): + """ + Update metadata with the given metadata dict or request them to + liquidsoap if nothing is given. + + Return -1 in case no update happened + """ + if metadata is None: + r = self._send(self.id, '.get', parse=True) + return self.update(metadata = r or {}) + + source = metadata.get('source') or '' + # FIXME: self.program + if hasattr(self, 'program') and self.program \ + and not source.startswith(self.id): + return -1 + self.metadata = metadata + return + + +class Source(BaseSource): + playlist = None # playlist file + program = None # related program (if given) + is_dealer = False # Source is a dealer + metadata = None + + def __init__(self, controller, program = None, is_dealer = None): + station = controller.station + if is_dealer: + id, name = '{}_dealer'.format(station.slug), \ + 'Dealer' + self.is_dealer = True + else: + id, name = '{}_stream_{}'.format(station.slug, program.id), \ + program.name + + super().__init__(controller, id, name) + + path = os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA, + station.slug, + self.id + '.m3u') + self.playlist = Playlist(path, program = program) + + @property + def on(self): + """ + Switch on-off; + """ + if not self.is_dealer: + raise RuntimeError('only dealers can do that') + r = self._send('var.get ', self.id, '_on') + return (r == 'true') + + @on.setter + def on(self, value): + if not self.is_dealer: + raise RuntimeError('only dealers can do that') + return self._send('var.set ', self.id, '_on', '=', + 'true' if value else 'false') + + def stream_info(self): """ Return a dict with info related to the program's stream. """ @@ -180,7 +241,7 @@ class Source: if not stream.begin and not stream.delay: return - def to_seconds (time): + def to_seconds(time): return 3600 * time.hour + 60 * time.minute + time.second return { @@ -189,119 +250,59 @@ class Source: 'delay': to_seconds(stream.delay) if stream.delay else 0 } - def skip (self): - """ - Skip a given source. If no source, use master. - """ - self.connector.send(self.id, '.skip') - def update (self, metadata = None): - """ - Update metadata with the given metadata dict or request them to - liquidsoap if nothing is given. - - Return -1 in case no update happened - """ - if metadata is not None: - source = metadata.get('source') or '' - if self.program and not source.startswith(self.id): - return -1 - self.metadata = metadata - return - - # r = self.connector.send('var.get ', self.id + '_meta', parse_json=True) - r = self.connector.send(self.id, '.get', parse=True) - return self.update(metadata = r or {}) - - -class Master (Source): +class Master (BaseSource): """ - A master Source + A master Source based on a given station """ - def update (self, metadata = None): + def __init__(self, controller): + station = controller.station + super().__init__(controller, station.slug, station.name) + + def update(self, metadata = None): if metadata is not None: return super().update(metadata) - r = self.connector.send('request.on_air') - r = self.connector.send('request.metadata ', r, parse = True) + r = self._send('request.on_air') + r = self._send('request.metadata ', r, parse = True) return self.update(metadata = r or {}) -class Dealer (Source): - """ - The Dealer source is a source that is used for scheduled diffusions and - manual sound diffusion. - - Since we need to cache buffers for the scheduled track, we use an on-off - switch in order to have no latency and enable preload. - """ - name = _('Dealer') - - @property - def id (self): - return self.station.slug + '_dealer' - - def stream_info (self): - pass - - @property - def on (self): - """ - Switch on-off; - """ - r = self.connector.send('var.get ', self.id, '_on') - return (r == 'true') - - @on.setter - def on (self, value): - return self.connector.send('var.set ', self.id, '_on', - '=', 'true' if value else 'false') - class Controller: """ Main class controller for station and sources (streams and dealer) """ + id = None + name = None + path = None + connector = None station = None # the related station master = None # master source (station's source) dealer = None # dealer source streams = None # streams streams + # FIXME: used nowhere except in liquidsoap cli to get on air item but is not + # correctly @property - def on_air (self): + def on_air(self): return self.master @property - def id (self): - return self.master and self.master.id - - @property - def name (self): - return self.master and self.master.name - - @property - def path (self): - """ - 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): + def socket_path(self): """ Connector's socket path """ return os.path.join(self.path, 'station.sock') @property - def config_path (self): + def config_path(self): """ Connector's socket path """ return os.path.join(self.path, 'station.liq') - def __init__ (self, station, connector = True, update = False): + def __init__(self, station, connector = True, update = False): """ Params: - station: managed station @@ -311,6 +312,10 @@ class Controller: to the given station; We ensure the existence of the controller's files dir. """ + self.id = station.slug + self.name = station.name + self.path = os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA, station.slug) + self.station = station self.station.controller = self self.outputs = models.Output.objects.filter(station = station) @@ -318,7 +323,7 @@ class Controller: self.connector = connector and Connector(self.socket_path) self.master = Master(self) - self.dealer = Dealer(self) + self.dealer = Source(self, is_dealer = True) self.streams = { source.id : source for source in [ @@ -332,7 +337,7 @@ class Controller: if update: self.update() - def get (self, source_id): + def get(self, source_id): """ Get a source by its id """ @@ -342,8 +347,7 @@ class Controller: return self.dealer return self.streams.get(source_id) - - def update (self): + def update(self): """ Fetch and update all streams metadata. """ @@ -352,6 +356,38 @@ class Controller: for source in self.streams.values(): source.update() + def write_data(self, playlist = True, config = True): + """ + Write stream's playlists, and config + """ + os.makedirs(self.path, exist_ok = True) + if playlist: + for source in self.streams.values(): + source.playlist.save() + self.dealer.playlist.save() + + if not config: + return + + log_script = main_settings.BASE_DIR \ + if hasattr(main_settings, 'BASE_DIR') else \ + main_settings.PROJECT_ROOT + log_script = os.path.join(log_script, 'manage.py') + \ + ' liquidsoap_log' + + context = { + 'controller': self, + 'settings': settings, + 'log_script': log_script, + } + + 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.config_path, 'w+') as file: + file.write(data) + class Monitor: """ @@ -359,7 +395,7 @@ class Monitor: """ controllers = None - def __init__ (self): + def __init__(self): self.controllers = { controller.id : controller for controller in [ @@ -368,7 +404,7 @@ class Monitor: ] } - def update (self): + def update(self): for controller in self.controllers.values(): controller.update() diff --git a/programs/management/__pycache__/__init__.cpython-34.pyc b/programs/management/__pycache__/__init__.cpython-34.pyc index 4759fc715a74f479bb39622e6c795d6cf1be6893..cdbd9eb7582a0be22f8ec8a7520d407175e50eed 100644 GIT binary patch delta 40 vcmbQt*vrWAj)#}aZem^7L=JNiEB)f4GX0E_k^=q2%%bG{iuj`BiIG+S|BMZT delta 46 zcmeBWoXp7aj)#}a=*E@Mi5%uq4*I#NDVd4-DTyVC`pNmFMTvPO#Xv?%>cn6x0B5Zb A7XSbN diff --git a/programs/management/commands/sounds_monitor.py b/programs/management/commands/sounds_monitor.py index 194b9bb..15240eb 100644 --- a/programs/management/commands/sounds_monitor.py +++ b/programs/management/commands/sounds_monitor.py @@ -133,9 +133,9 @@ class SoundInfo: # check on episodes diffusion = Diffusion.objects.filter( program = program, - date__year = self.year, - date__month = self.month, - date__day = self.day, + start__year = self.year, + start__month = self.month, + start__day = self.day, initial = None, ) if not diffusion: