fix errors, update a bit how liquidsoap part work and so on

This commit is contained in:
bkfox 2016-05-16 18:43:36 +02:00
parent 032bd6c56d
commit 29d0929a0c
5 changed files with 264 additions and 277 deletions

View File

@ -23,70 +23,6 @@ import aircox.liquidsoap.settings as settings
import aircox.liquidsoap.utils as utils 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: class Monitor:
@classmethod @classmethod
def run (cl, controller): def run (cl, controller):
@ -128,6 +64,7 @@ class Monitor:
diffusion.playlist = [ sound.path diffusion.playlist = [ sound.path
for sound in diffusion.get_archives() ] for sound in diffusion.get_archives() ]
diffusion.playlist.save()
if diffusion.playlist and on_air not in diffusion.playlist: if diffusion.playlist and on_air not in diffusion.playlist:
return diffusion return diffusion
@ -152,6 +89,7 @@ class Monitor:
on_air not in playlist: on_air not in playlist:
dealer.on = False dealer.on = False
dealer.playlist = diff.playlist dealer.playlist = diff.playlist
dealer.playlist.save()
# run the diff # run the diff
if dealer.playlist == diff.playlist and diff.start <= now and not dealer.on: 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' help='run liquidsoap on exit'
) )
group = parser.add_argument_group('monitor') group = parser.add_argument_group('actions')
group.add_argument( group.add_argument(
'-o', '--on_air', action='store_true', '-d', '--delay', type=int,
help='print what is on air' default=1000,
help='time to sleep in milliseconds between two updates when we '
'monitor'
) )
group.add_argument( group.add_argument(
'-m', '--monitor', action='store_true', '-m', '--monitor', action='store_true',
help='run in monitor mode' help='run in monitor mode'
) )
group.add_argument( group.add_argument(
'-d', '--delay', type=int, '-o', '--on_air', action='store_true',
default=1000, help='print what is on air'
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'
) )
group.add_argument( group.add_argument(
'-r', '--run', action='store_true', '-r', '--run', action='store_true',
help='run liquidsoap with the generated configuration' 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): def handle (self, *args, **options):
# selector
stations = [] stations = []
if options.get('station'): if options.get('all'):
stations = [ StationConfig( stations = programs.Station.objects.filter(active = True)
programs.Station.objects.get( elif options.get('station'):
id = options.get('station') stations = programs.Station.objects.filter(
)) ] id__in = 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)
]
run = options.get('run') run = options.get('run')
for station in stations: monitor = options.get('on_air') or options.get('monitor')
station.handle(options)
if run:
station.run()
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) self.handle_monitor(options)
# post
if run: if run:
for station in stations: for controller in self.controllers:
station.process.wait() 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): def handle_monitor (self, options):
controllers = [ controllers = [

View File

@ -31,7 +31,7 @@ set("{{ key|safe }}", {{ value|safe }}) \
at(interactive.bool('{{ source.id }}_on', false), \ at(interactive.bool('{{ source.id }}_on', false), \
interactive_source('{{ source.id }}', playlist.once( \ interactive_source('{{ source.id }}', playlist.once( \
reload_mode='watch', \ reload_mode='watch', \
"{{ source.path }}", \ "{{ source.playlist.path }}", \
)) \ )) \
), \ ), \
{% endif %} {% endif %}
@ -41,17 +41,21 @@ set("{{ key|safe }}", {{ value|safe }}) \
interactive_source("{{ controller.id }}_streams", rotate([ \ interactive_source("{{ controller.id }}_streams", rotate([ \
{% for source in controller.streams.values %} {% for source in controller.streams.values %}
{% with info=source.stream_info %} {% with info=source.stream_info %}
{% with path=source.playlist.path %}
{% if info.delay %} {% if info.delay %}
delay({{ info.delay }}., stream("{{ source.id }}", "{{ source.path }}")), \ delay({{ info.delay }}., stream("{{ source.id }}", "{{ path }}")), \
{% elif info.begin and info.end %} {% elif info.begin and info.end %}
at({ {{info.begin}}-{{info.end}} }, stream("{{ source.id }}", "{{ source.path }}")), \ at({ {{info.begin}}-{{info.end}} }, stream("{{ source.id }}", "{{ path }}")), \
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% endwith %}
{% endfor %} {% endfor %}
{% for source in controller.streams.values %} {% for source in controller.streams.values %}
{% if not source.stream_info %} {% if not source.stream_info %}
{% with path=source.playlist.path %}
stream("{{ source.id }}", "{{ source.path }}"), \ stream("{{ source.id }}", "{{ source.path }}"), \
{% endwith %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
])), \ ])), \

View File

@ -2,11 +2,15 @@ import os
import socket import socket
import re import re
import json import json
import subprocess
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils import timezone as tz 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.models as programs
import aircox.programs.settings as programs_settings
import aircox.liquidsoap.models as models import aircox.liquidsoap.models as models
import aircox.liquidsoap.settings as settings import aircox.liquidsoap.settings as settings
@ -23,14 +27,14 @@ class Connector:
address = None address = None
@property @property
def available (self): def available(self):
return self.__available return self.__available
def __init__ (self, address = None): def __init__(self, address = None):
if address: if address:
self.address = address self.address = address
def open (self): def open(self):
if self.__available: if self.__available:
return return
@ -45,7 +49,7 @@ class Connector:
self.__available = False self.__available = False
return -1 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(): if self.open():
return '' return ''
data = bytes(''.join([str(d) for d in data]) + '\n', encoding='utf-8') data = bytes(''.join([str(d) for d in data]) + '\n', encoding='utf-8')
@ -71,7 +75,7 @@ class Connector:
if try_count > 0: if try_count > 0:
return self.send(data, try_count - 1) return self.send(data, try_count - 1)
def parse (self, string): def parse(self, string):
string = string.split('\n') string = string.split('\n')
data = {} data = {}
for line in string: for line in string:
@ -82,7 +86,7 @@ class Connector:
data[line['key']] = line['value'] data[line['key']] = line['value']
return data return data
def parse_json (self, string): def parse_json(self, string):
try: try:
if string[0] == '"' and string[-1] == '"': if string[0] == '"' and string[-1] == '"':
string = string[1:-1] string = string[1:-1]
@ -91,85 +95,142 @@ class Connector:
return None return None
class 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):
""" """
A structure that holds informations about a LiquidSoap source. 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 controller = None
program = None
metadata = 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.controller = controller
self.program = program
def _send(self, *args, **kwargs):
self.controller.connector.send(*args, **kwargs)
@property @property
def station (self): def current_sound(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):
self.update() self.update()
return self.metadata.get('initial_uri') if self.metadata else {} 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. 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: if not stream.begin and not stream.delay:
return return
def to_seconds (time): def to_seconds(time):
return 3600 * time.hour + 60 * time.minute + time.second return 3600 * time.hour + 60 * time.minute + time.second
return { return {
@ -189,119 +250,59 @@ class Source:
'delay': to_seconds(stream.delay) if stream.delay else 0 '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): class Master (BaseSource):
""" """
Update metadata with the given metadata dict or request them to A master Source based on a given station
liquidsoap if nothing is given.
Return -1 in case no update happened
""" """
if metadata is not None: def __init__(self, controller):
source = metadata.get('source') or '' station = controller.station
if self.program and not source.startswith(self.id): super().__init__(controller, station.slug, station.name)
return -1
self.metadata = metadata
return
# r = self.connector.send('var.get ', self.id + '_meta', parse_json=True) def update(self, metadata = None):
r = self.connector.send(self.id, '.get', parse=True)
return self.update(metadata = r or {})
class Master (Source):
"""
A master Source
"""
def update (self, metadata = None):
if metadata is not None: if metadata is not None:
return super().update(metadata) return super().update(metadata)
r = self.connector.send('request.on_air') r = self._send('request.on_air')
r = self.connector.send('request.metadata ', r, parse = True) r = self._send('request.metadata ', r, parse = True)
return self.update(metadata = r or {}) 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: class Controller:
""" """
Main class controller for station and sources (streams and dealer) Main class controller for station and sources (streams and dealer)
""" """
id = None
name = None
path = None
connector = None connector = None
station = None # the related station station = None # the related station
master = None # master source (station's source) master = None # master source (station's source)
dealer = None # dealer source dealer = None # dealer source
streams = None # streams streams streams = None # streams streams
# FIXME: used nowhere except in liquidsoap cli to get on air item but is not
# correctly
@property @property
def on_air (self): def on_air(self):
return self.master return self.master
@property @property
def id (self): def socket_path(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):
""" """
Connector's socket path Connector's socket path
""" """
return os.path.join(self.path, 'station.sock') return os.path.join(self.path, 'station.sock')
@property @property
def config_path (self): def config_path(self):
""" """
Connector's socket path Connector's socket path
""" """
return os.path.join(self.path, 'station.liq') 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: Params:
- station: managed station - station: managed station
@ -311,6 +312,10 @@ class Controller:
to the given station; We ensure the existence of the controller's to the given station; We ensure the existence of the controller's
files dir. 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 = station
self.station.controller = self self.station.controller = self
self.outputs = models.Output.objects.filter(station = station) self.outputs = models.Output.objects.filter(station = station)
@ -318,7 +323,7 @@ class Controller:
self.connector = connector and Connector(self.socket_path) self.connector = connector and Connector(self.socket_path)
self.master = Master(self) self.master = Master(self)
self.dealer = Dealer(self) self.dealer = Source(self, is_dealer = True)
self.streams = { self.streams = {
source.id : source source.id : source
for source in [ for source in [
@ -332,7 +337,7 @@ class Controller:
if update: if update:
self.update() self.update()
def get (self, source_id): def get(self, source_id):
""" """
Get a source by its id Get a source by its id
""" """
@ -342,8 +347,7 @@ class Controller:
return self.dealer return self.dealer
return self.streams.get(source_id) return self.streams.get(source_id)
def update(self):
def update (self):
""" """
Fetch and update all streams metadata. Fetch and update all streams metadata.
""" """
@ -352,6 +356,38 @@ class Controller:
for source in self.streams.values(): for source in self.streams.values():
source.update() 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: class Monitor:
""" """
@ -359,7 +395,7 @@ class Monitor:
""" """
controllers = None controllers = None
def __init__ (self): def __init__(self):
self.controllers = { self.controllers = {
controller.id : controller controller.id : controller
for controller in [ for controller in [
@ -368,7 +404,7 @@ class Monitor:
] ]
} }
def update (self): def update(self):
for controller in self.controllers.values(): for controller in self.controllers.values():
controller.update() controller.update()

View File

@ -133,9 +133,9 @@ class SoundInfo:
# check on episodes # check on episodes
diffusion = Diffusion.objects.filter( diffusion = Diffusion.objects.filter(
program = program, program = program,
date__year = self.year, start__year = self.year,
date__month = self.month, start__month = self.month,
date__day = self.day, start__day = self.day,
initial = None, initial = None,
) )
if not diffusion: if not diffusion: