feat(playout): build liquidsoap entrypoint with stream config

This commit is contained in:
jo 2022-08-10 17:33:22 +02:00 committed by Kyle Robbertze
parent d9920a1196
commit b9368d1b7b
15 changed files with 486 additions and 120 deletions

View file

@ -1,6 +1,11 @@
from pathlib import Path
from libretime_shared.config import BaseConfig, GeneralConfig, RabbitMQConfig
from libretime_shared.config import (
BaseConfig,
GeneralConfig,
RabbitMQConfig,
StreamConfig,
)
from pydantic import BaseModel
from typing_extensions import Literal
@ -26,3 +31,4 @@ class Config(BaseConfig):
general: GeneralConfig
rabbitmq: RabbitMQConfig = RabbitMQConfig()
playout: PlayoutConfig = PlayoutConfig()
stream: StreamConfig = StreamConfig()

View file

@ -1,28 +1,11 @@
%include "/etc/libretime/liquidsoap.cfg"
if (log_file != "") then
set("log.file.path", log_file)
else
set("log.file", false)
end
set("server.telnet", true)
set("server.telnet.port", 1234)
time = ref string_of(gettimeofday())
#Dynamic source list
#dyn_sources = ref []
webstream_enabled = ref false
time = ref string_of(gettimeofday())
#live stream setup
set("harbor.bind_addr", "0.0.0.0")
current_dyn_id = ref '-1'
stream_metadata_type = ref 0
default_dj_fade = ref 0.
station_name = ref ''
show_name = ref ''
dynamic_metadata_callback = ref fun (s) -> begin () end

View file

@ -1,28 +1,11 @@
%include "/etc/libretime/liquidsoap.cfg"
if (log_file != "") then
set("log.file.path", log_file)
else
set("log.file", false)
end
set("server.telnet", true)
set("server.telnet.port", 1234)
time = ref string_of(gettimeofday())
#Dynamic source list
#dyn_sources = ref []
webstream_enabled = ref false
time = ref string_of(gettimeofday())
#live stream setup
set("harbor.bind_addrs", ["0.0.0.0"])
current_dyn_id = ref '-1'
stream_metadata_type = ref 0
default_dj_fade = ref 0.
station_name = ref ''
show_name = ref ''
dynamic_metadata_callback = ref fun (s) -> begin () end

View file

@ -1,28 +1,11 @@
%include "/etc/libretime/liquidsoap.cfg"
if (log_file != "") then
set("log.file.path", log_file)
else
set("log.file", false)
end
set("server.telnet", true)
set("server.telnet.port", 1234)
time = ref string_of(gettimeofday())
#Dynamic source list
#dyn_sources = ref []
webstream_enabled = ref false
time = ref string_of(gettimeofday())
#live stream setup
set("harbor.bind_addrs", ["0.0.0.0"])
current_dyn_id = ref '-1'
stream_metadata_type = ref 0
default_dj_fade = ref 0.
station_name = ref ''
show_name = ref ''
dynamic_metadata_callback = ref fun (~new_track=false, s) -> begin () end

View file

@ -0,0 +1,61 @@
# THIS FILE IS AUTO GENERATED. PLEASE DO NOT EDIT!
###########################################################
# The ignore() lines are to squash unused variable warnings
# Inputs
master_live_stream_mp = "{{ config.stream.inputs.main.mount }}"
master_live_stream_port = {{ config.stream.inputs.main.port }}
dj_live_stream_mp = "{{ config.stream.inputs.show.mount }}"
dj_live_stream_port = {{ config.stream.inputs.show.port }}
{% for output in config.stream.outputs.merged -%}
# Output s{{ loop.index }}
s{{ loop.index }}_enable = {{ output.enabled | lower }}
s{{ loop.index }}_output = "{{ output.kind }}"
s{{ loop.index }}_host = "{{ output.host }}"
s{{ loop.index }}_port = {{ output.port }}
s{{ loop.index }}_mount = "{{ output.mount }}"
s{{ loop.index }}_user = "{{ output.source_user }}"
s{{ loop.index }}_pass = "{{ output.source_password }}"
s{{ loop.index }}_channels = "{{ output.audio.channels.value }}"
s{{ loop.index }}_type = "{{ output.audio.format.value }}"
s{{ loop.index }}_bitrate = {{ output.audio.bitrate }}
s{{ loop.index }}_name = "{{ output.name or '' }}"
s{{ loop.index }}_description = "{{ output.description or '' }}"
s{{ loop.index }}_genre = "{{ output.genre or '' }}"
s{{ loop.index }}_url = "{{ output.website or '' }}"
{% endfor -%}
icecast_vorbis_metadata = {{ icecast_vorbis_metadata | lower }}
# System output
output_sound_device = {{ config.stream.outputs.system[0].enabled | lower }}
output_sound_device_type = "{{ config.stream.outputs.system[0].kind.value }}"
# Settings
auth_path = "{{ paths.auth_filepath }}"
{% if paths.log_filepath is defined -%}
set("log.file.path", "{{ paths.log_filepath }}")
{%- else -%}
set("log.file", false)
{%- endif %}
set("server.telnet", true)
set("server.telnet.port", 1234)
{% if version >= (1, 3, 3) -%}
set("harbor.bind_addrs", ["0.0.0.0"])
{%- else -%}
set("harbor.bind_addr", "0.0.0.0")
{%- endif %}
station_name = ref "{{ info.station_name }}"
off_air_meta = "{{ preferences.message_offline }}"
stream_metadata_type = ref {{ preferences.message_format.value }}
default_dj_fade = ref {{ preferences.input_fade_transition }}
%include "{{ paths.lib_filepath }}"

View file

@ -1,64 +1,70 @@
import os
import sys
import time
from pathlib import Path
from typing import Optional
from typing import Optional, Tuple
from libretime_api_client.v1 import ApiClient as LegacyClient
from loguru import logger
from jinja2 import Template
from libretime_shared.config import AudioFormat, IcecastOutput, SystemOutput
from ..config import Config
from .models import Info, StreamPreferences
here = Path(__file__).parent
entrypoint_template_path = here / "entrypoint.liq.j2"
entrypoint_template = Template(
entrypoint_template_path.read_text(encoding="utf-8"),
keep_trailing_newline=True,
)
# Liquidsoap has 4 hardcoded output stream set of variables, so we need to
# fill the missing stream outputs with placeholders so Liquidsoap does
# not fail with missing variables in the entrypoint.
_icecast_placeholder = IcecastOutput(
enabled=False,
mount="",
source_password="",
audio=dict(format="ogg", bitrate=256),
)
_system_placeholder = SystemOutput()
def generate_liquidsoap_config(ss, log_filepath: Optional[Path]):
data = ss["msg"]
fh = open("/etc/libretime/liquidsoap.cfg", "w")
fh.write("################################################\n")
fh.write("# THIS FILE IS AUTO GENERATED. DO NOT CHANGE!! #\n")
fh.write("################################################\n")
fh.write("# The ignore() lines are to squash unused variable warnings\n")
def generate_entrypoint(
entrypoint_filepath: Path,
log_filepath: Optional[Path],
config: Config,
preferences: StreamPreferences,
info: Info,
version: Tuple[int, int, int],
):
paths = {}
paths["auth_filepath"] = here / "liquidsoap_auth.py"
paths["lib_filepath"] = here / f"{version[0]}.{version[1]}/ls_script.liq"
for key, value in data.items():
try:
if not "port" in key and not "bitrate" in key: # Stupid hack
raise ValueError()
str_buffer = f"{key} = {int(value)}\n"
except ValueError:
try: # Is it a boolean?
if value == "true" or value == "false":
str_buffer = f"{key} = {value.lower()}\n"
else:
raise ValueError() # Just drop into the except below
except: # Everything else is a string
str_buffer = f'{key} = "{value}"\n'
if log_filepath is not None:
paths["log_filepath"] = log_filepath.resolve()
fh.write(str_buffer)
# ignore squashes unused variable errors from Liquidsoap
fh.write("ignore(%s)\n" % key)
config = config.copy()
missing_outputs = [_icecast_placeholder] * (4 - len(config.stream.outputs.merged))
config.stream.outputs.icecast.extend(missing_outputs)
auth_path = os.path.dirname(os.path.realpath(__file__))
log_file = log_filepath.resolve() if log_filepath is not None else ""
if not config.stream.outputs.system:
config.stream.outputs.system.append(_system_placeholder)
fh.write(f'log_file = "{log_file}"\n')
fh.write('auth_path = "%s/liquidsoap_auth.py"\n' % auth_path)
fh.close()
# Global icecast_vorbis_metadata until it is
# handled per output
icecast_vorbis_metadata = any(
o.enabled and o.audio.format == AudioFormat.OGG and o.audio.enable_metadata # type: ignore
for o in config.stream.outputs.icecast
)
def generate_entrypoint(log_filepath: Optional[Path]):
attempts = 0
max_attempts = 10
successful = False
while not successful:
try:
legacy_client = LegacyClient(logger)
ss = legacy_client.get_stream_setting()
generate_liquidsoap_config(ss, log_filepath)
successful = True
except Exception:
logger.exception("Unable to connect to the Airtime server")
if attempts == max_attempts:
logger.error("giving up and exiting...")
sys.exit(1)
else:
logger.info("Retrying in 3 seconds...")
time.sleep(3)
attempts += 1
entrypoint_filepath.write_text(
entrypoint_template.render(
config=config,
preferences=preferences,
info=info,
paths=paths,
version=version,
icecast_vorbis_metadata=icecast_vorbis_metadata,
),
encoding="utf-8",
)

View file

@ -3,12 +3,15 @@ from pathlib import Path
from typing import Optional
import click
from libretime_shared.cli import cli_logging_options
from libretime_api_client.v2 import ApiClient
from libretime_shared.cli import cli_config_options, cli_logging_options
from libretime_shared.config import DEFAULT_ENV_PREFIX
from libretime_shared.logging import level_from_name, setup_logger
from loguru import logger
from ..config import Config
from .entrypoint import generate_entrypoint
from .models import Info, StreamPreferences
from .version import get_liquidsoap_version
here = Path(__file__).parent
@ -16,25 +19,42 @@ here = Path(__file__).parent
@click.command(context_settings={"auto_envvar_prefix": DEFAULT_ENV_PREFIX})
@cli_logging_options()
def cli(log_level: str, log_filepath: Optional[Path]):
@cli_config_options()
def cli(log_level: str, log_filepath: Optional[Path], config_filepath: Optional[Path]):
"""
Run liquidsoap.
"""
logger_level, _ = setup_logger(level_from_name(log_level), log_filepath)
config = Config(config_filepath)
generate_entrypoint(log_filepath)
api_client = ApiClient(
base_url=config.general.public_url,
api_key=config.general.api_key,
)
version = get_liquidsoap_version()
script_path = here / f"{version[0]}.{version[1]}/ls_script.liq"
info = Info(**api_client.get_info().json())
preferences = StreamPreferences(**api_client.get_stream_preferences().json())
entrypoint_filepath = Path.cwd() / "radio.liq"
generate_entrypoint(
entrypoint_filepath,
log_filepath,
config,
preferences,
info,
version,
)
exec_args = [
"/usr/bin/liquidsoap",
"libretime-liquidsoap",
"--verbose",
str(script_path),
str(entrypoint_filepath),
]
if logger_level.is_debug():
exec_args.append("--debug")
logger.debug(f"Liquidsoap {version} using script: {script_path}")
logger.debug(f"liquidsoap {version} using script: {entrypoint_filepath}")
os.execl(*exec_args)

View file

@ -0,0 +1,27 @@
from enum import Enum
from pydantic import BaseModel
class Info(BaseModel):
station_name: str
class MessageFormatKind(int, Enum):
ARTIST_TITLE = 0
SHOW_ARTIST_TITLE = 1
RADIO_SHOW = 2
class StreamPreferences(BaseModel):
input_fade_transition: float
message_format: MessageFormatKind
message_offline: str
class StreamState(BaseModel):
input_main_connected: bool
input_main_streaming: bool
input_show_connected: bool
input_show_streaming: bool
schedule_streaming: bool