merge liquidsoap commands, different instances of liquidsoap for each station
This commit is contained in:
parent
202992521e
commit
012e2dd9d0
|
@ -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 time
|
||||||
|
import re
|
||||||
from argparse import RawTextHelpFormatter
|
from argparse import RawTextHelpFormatter
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from django.utils import timezone as tz
|
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.settings as settings
|
||||||
import aircox.liquidsoap.utils as utils
|
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):
|
class Command (BaseCommand):
|
||||||
|
@ -18,38 +66,69 @@ class Command (BaseCommand):
|
||||||
|
|
||||||
def add_arguments (self, parser):
|
def add_arguments (self, parser):
|
||||||
parser.formatter_class=RawTextHelpFormatter
|
parser.formatter_class=RawTextHelpFormatter
|
||||||
parser.add_argument(
|
|
||||||
|
group = parser.add_argument_group('monitor')
|
||||||
|
group.add_argument(
|
||||||
'-o', '--on_air', action='store_true',
|
'-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',
|
'-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,
|
'-d', '--delay', type=int,
|
||||||
default=1000,
|
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):
|
def handle (self, *args, **options):
|
||||||
connector = utils.Connector()
|
if options.get('station'):
|
||||||
self.monitor = utils.Monitor(connector)
|
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()
|
self.monitor.update()
|
||||||
|
|
||||||
if options.get('on_air'):
|
if options.get('on_air'):
|
||||||
for id, controller in self.monitor.controller.items():
|
for id, controller in self.monitor.controller.items():
|
||||||
print(id, controller.on_air)
|
print(id, controller.on_air)
|
||||||
|
return
|
||||||
|
|
||||||
if options.get('monitor'):
|
if options.get('monitor'):
|
||||||
delay = options.get('delay') / 1000
|
delay = options.get('delay') / 1000
|
||||||
while True:
|
while True:
|
||||||
for controller in self.monitor.controllers.values():
|
for controller in self.monitor.controllers.values():
|
||||||
try:
|
controller.monitor()
|
||||||
controller.monitor()
|
|
||||||
except Exception, e:
|
|
||||||
print(e)
|
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,6 @@ def ensure (key, default):
|
||||||
globals()[key] = getattr(settings, 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)
|
# dict of values to set (do not forget to escape chars)
|
||||||
ensure('AIRCOX_LIQUIDSOAP_SET', {
|
ensure('AIRCOX_LIQUIDSOAP_SET', {
|
||||||
'log.file.path': '"/tmp/liquidsoap.log"',
|
'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
|
# start the server on monitor if not present
|
||||||
ensure('AIRCOX_LIQUIDSOAP_AUTOSTART', True)
|
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')
|
ensure('AIRCOX_LIQUIDSOAP_MEDIA', '/tmp')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
{# Utilities #}
|
{# Context: #}
|
||||||
|
{# - controller: controller used to generate the current file #}
|
||||||
|
{# - settings: global settings #}
|
||||||
|
|
||||||
def interactive_source (id, s) = \
|
def interactive_source (id, s) = \
|
||||||
s = store_metadata(id=id, size=1, s) \
|
s = store_metadata(id=id, size=1, s) \
|
||||||
add_skip_command(s) \
|
add_skip_command(s) \
|
||||||
|
@ -13,13 +16,11 @@ end \
|
||||||
\
|
\
|
||||||
{# Config #}
|
{# Config #}
|
||||||
set("server.socket", true) \
|
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 %}
|
{% for key, value in settings.AIRCOX_LIQUIDSOAP_SET.items %}
|
||||||
set("{{ key|safe }}", {{ value|safe }}) \
|
set("{{ key|safe }}", {{ value|safe }}) \
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
\
|
\
|
||||||
\
|
|
||||||
{% for controller in monitor.controllers.values %}
|
|
||||||
{# station #}
|
{# station #}
|
||||||
{{ controller.id }} = interactive_source ( \
|
{{ controller.id }} = interactive_source ( \
|
||||||
"{{ controller.id }}", \
|
"{{ controller.id }}", \
|
||||||
|
@ -72,5 +73,4 @@ output.{{ output.get_type_display }}( \
|
||||||
{% endif %}
|
{% endif %}
|
||||||
) \
|
) \
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Connector:
|
||||||
"""
|
"""
|
||||||
__socket = None
|
__socket = None
|
||||||
__available = False
|
__available = False
|
||||||
address = settings.AIRCOX_LIQUIDSOAP_SOCKET
|
address = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available (self):
|
def available (self):
|
||||||
|
@ -144,6 +144,7 @@ class Source:
|
||||||
"""
|
"""
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
settings.AIRCOX_LIQUIDSOAP_MEDIA,
|
settings.AIRCOX_LIQUIDSOAP_MEDIA,
|
||||||
|
self.station.slug,
|
||||||
self.id + '.m3u'
|
self.id + '.m3u'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -313,6 +314,9 @@ class Dealer (Source):
|
||||||
|
|
||||||
|
|
||||||
class Controller:
|
class Controller:
|
||||||
|
"""
|
||||||
|
Main class controller for station and sources (streams and dealer)
|
||||||
|
"""
|
||||||
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)
|
||||||
|
@ -331,15 +335,45 @@ class Controller:
|
||||||
def name (self):
|
def name (self):
|
||||||
return self.master and self.master.name
|
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 = 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)
|
||||||
|
|
||||||
|
os.makedirs(self.path, exist_ok = True)
|
||||||
|
self.connector = connector and Connector(self.socket_path)
|
||||||
|
|
||||||
self.master = Master(self)
|
self.master = Master(self)
|
||||||
self.dealer = Dealer(self)
|
self.dealer = Dealer(self)
|
||||||
self.streams = {
|
self.streams = {
|
||||||
|
@ -419,11 +453,11 @@ class Monitor:
|
||||||
"""
|
"""
|
||||||
controllers = None
|
controllers = None
|
||||||
|
|
||||||
def __init__ (self, connector = None):
|
def __init__ (self):
|
||||||
self.controllers = {
|
self.controllers = {
|
||||||
controller.id : controller
|
controller.id : controller
|
||||||
for controller in [
|
for controller in [
|
||||||
Controller(station, connector)
|
Controller(station, True)
|
||||||
for station in programs.Station.objects.filter(active = True)
|
for station in programs.Station.objects.filter(active = True)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,8 @@ view_monitor = None
|
||||||
|
|
||||||
def get_monitor():
|
def get_monitor():
|
||||||
global view_monitor
|
global view_monitor
|
||||||
|
|
||||||
if not view_monitor:
|
if not view_monitor:
|
||||||
view_monitor = utils.Monitor(
|
view_monitor = utils.Monitor()
|
||||||
utils.Connector(address = settings.AIRCOX_LIQUIDSOAP_SOCKET)
|
|
||||||
)
|
|
||||||
return view_monitor
|
return view_monitor
|
||||||
|
|
||||||
class Actions:
|
class Actions:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user