feat(playout): load config using shared helpers

- backport >=py38 Literal type
- update configuration sample
- add missing config filepath to systemd service
- make config sections optional

BREAKING CHANGE: the playout config schema has been changed like the following:
- the top section 'pypo' was renamed to 'playout'
- the 'playout.ls_*' entries were renamed to 'playout.liquidsoap_*'
- the 'playout.*_dir' entries were removed
- the 'playout.api_client' entry was removed
- the 'playout.record_file_type' entry was renamed to 'playout.record_file_format'
- the 'playout.base_recorded_files' entry was removed
- the 'playout.poll_interval' entry was removed
- the 'playout.push_interval' entry was removed
- the 'playout.cue_style' entry was removed
This commit is contained in:
jo 2022-01-18 20:59:11 +01:00 committed by Kyle Robbertze
parent 12f96f5043
commit fa7692071c
12 changed files with 142 additions and 214 deletions

View File

@ -9,7 +9,6 @@
#
# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
# G E N E R A L S E T T I N G S
# ----------------------------------------------------------------------
@ -73,7 +72,6 @@ auth = local
#
# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
# D A T A B A S E
# ----------------------------------------------------------------------
@ -100,38 +98,21 @@ dbpass = airtime
#
# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
# RABBITMQ
# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
# R A B B I T M Q
# ----------------------------------------------------------------------
#
# These settings are used to configure the RabbitMQ messaging
# configuration for your Airtime installation.
#
# host: The IP address for the RabbitMQ service.
# The default is 127.0.0.1.
#
# port: The port for the RabbitMQ service.
# The default is 5672.
#
# user: The username for the RabbitMQ user.
# The default is airtime.
#
# password: The password for the RabbitMQ user.
# The default is airtime.
#
# vhost: The virtual host for the RabbitMQ service database.
# The default is /airtime.
#
[rabbitmq]
host = 127.0.0.1
# The host for the RabbitMQ service, default is localhost
host = localhost
# The port for the RabbitMQ service, default is 5672
port = 5672
user = airtime
password = airtime
# The virtual host for the RabbitMQ service, default is /libretime
vhost = /airtime
#
# ----------------------------------------------------------------------
# The user for the RabbitMQ user, default is libretime
user = airtime
# The password for the RabbitMQ user, default is libretime
password = airtime
# ----------------------------------------------------------------------
# S T O R A G E
@ -140,7 +121,6 @@ vhost = /airtime
[current_backend]
storage_backend=file
# ----------------------------------------------------------------------
# M O N I T
# ----------------------------------------------------------------------
@ -157,115 +137,26 @@ password =
#
# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
# P Y P O
# PLAYOUT
# ----------------------------------------------------------------------
#
# api_client: Set the type of client you are using.
# Currently supported types:
# 1) 'obp' = Open Broadcast Platform
# 2) 'airtime'
# The default is 'airtime'
#
# cache_dir: The directory for pypo cache files
# The default is '/var/tmp/airtime/pypo/cache/'
#
# file_dir: The directory for pypo media files
# The default is '/var/tmp/airtime/pypo/files/'
#
# tmp_dir: The directory for pypo temp files
# The default is '/var/tmp/airtime/pypo/tmp/'
#
# cache_base_dir: The pypo base cache directory
# The default is '/var/tmp/airtime/pypo/'
#
# log_base_dir: The base directory for Airtime log files
# The default is '/var/log/airtime'
#
# pypo_log_dir: The directory for pypo log files
# The default is '/var/log/airtime/pypo'
#
# liquidsoap_log_dir: The directory for liquidsoap log files
# The default is '/var/log/airtime/pypo-liquidsoap'
#
# ls_host: Liquidsoap connection host
# The default is '127.0.0.1'
#
# ls_port: Liquidsoap connection port
# The default is '1234'
#
# poll_interval: Poll interval in seconds
#
# This will rarely need to be changed because any schedule
# changes are automatically sent to pypo immediately
# This is how often the poll script downloads new schedules
# and files from the server in the event that no changes
# are made to the schedule
# The default is 3600
#
# push_interval: Push interval in seconds
#
# This is how often the push script checks whether it has
# something new to push to liquidsoap
# The default is 1
#
# cue_style: Can be set to 'pre' or 'otf'
# 'pre' cues while playlist preparation
# 'otf' (on the fly) cues while loading into ls
# (needs the post_processor patch)
# The default is 'pre'
#
# record_bitrate: The bitrate for recordings
# The default is 256
#
# record_samplerate: The samplerate for recordings
# The default is 44100
#
# record_channels: The number of channels for recordings
# The default is 2
#
# record_sample_size: The sample size for recordings
# The default is 16
#
# record_file_type: Can be either ogg|mp3, mp3 recording requires
# installation of the package "lame"
# The default is ogg
#
# base_recorded_files: Base path to store recordered shows at
# The default is '/var/tmp/airtime/show-recorder/'
#
[pypo]
api_client = 'airtime'
# ---------- Cache directories - !! Include trailing slash !! ----------
cache_dir = '/var/tmp/airtime/pypo/cache/'
file_dir = '/var/tmp/airtime/pypo/files/'
tmp_dir = '/var/tmp/airtime/pypo/tmp/'
# ------- Setup directories - !! Don't include trailing slash !! -------
cache_base_dir = '/var/tmp/airtime/pypo'
log_base_dir = '/var/log/airtime'
pypo_log_dir = '/var/log/airtime/pypo'
liquidsoap_log_dir = '/var/log/airtime/pypo-liquidsoap'
# ------------------------ Liquidsoap Settings -------------------------
ls_host = '127.0.0.1'
ls_port = '1234'
# -------------------------- Pypo Preferences --------------------------
poll_interval = 3600
push_interval = 1
cue_style = 'pre'
# ---------------------- Recorded Audio Settings -----------------------
[playout]
# Liquidsoap connection host, default is localhost
liquidsoap_host = localhost
# Liquidsoap connection port, default is 1234
liquidsoap_port = 1234
# The format for recordings, allowed values ogg|mp3, default is ogg
record_file_format = ogg
# The bitrate for recordings, default is 256
record_bitrate = 256
# The samplerate for recordings, default is 256
record_samplerate = 44100
# The number of channels for recordings, default is 2
record_channels = 2
# The sample size for recordings, default is 16
record_sample_size = 16
record_file_type = 'ogg'
base_recorded_files = '/var/tmp/airtime/show-recorder/'
#
# ----------------------------------------------------------------------
#
# ----------------------------------------------------------------------
# F A C E B O O K
# ----------------------------------------------------------------------

View File

@ -3,6 +3,7 @@ Description=Libretime Liquidsoap Service
[Service]
Environment=LIBRETIME_LOG_FILEPATH=/var/log/libretime/liquidsoap.log
Environment=LIBRETIME_CONFIG_FILEPATH=/etc/airtime/airtime.conf
ExecStart=/usr/local/bin/libretime-liquidsoap
User=libretime-playout

View File

@ -4,6 +4,7 @@ After=network-online.target
[Service]
Environment=LIBRETIME_LOG_FILEPATH=/var/log/libretime/playout.log
Environment=LIBRETIME_CONFIG_FILEPATH=/etc/airtime/airtime.conf
WorkingDirectory=/var/lib/libretime/playout
ExecStart=/usr/local/bin/libretime-playout

View File

@ -1,4 +1,24 @@
from pathlib import Path
from libretime_shared.config import BaseConfig, RabbitMQConfig
from pydantic import BaseModel
from typing_extensions import Literal
CACHE_DIR = Path.cwd() / "scheduler"
RECORD_DIR = Path.cwd() / "recorder"
class PlayoutConfig(BaseModel):
liquidsoap_host: str = "localhost"
liquidsoap_port: int = 1234
record_file_format: Literal["mp3", "ogg"] = "ogg" # record_file_type
record_bitrate: int = 256
record_samplerate: int = 44100
record_channels: int = 2
record_sample_size: int = 16
class Config(BaseConfig):
rabbitmq = RabbitMQConfig()
playout = PlayoutConfig()

View File

@ -11,12 +11,14 @@ import defusedxml.minidom
from libretime_api_client import version1 as api_client
from loguru import logger
from .config import Config
class ListenerStat(Thread):
HTTP_REQUEST_TIMEOUT = 30 # 30 second HTTP request timeout
def __init__(self, config):
def __init__(self, config: Config):
Thread.__init__(self)
self.config = config
self.api_client = api_client.AirtimeApiClient()

View File

@ -16,15 +16,14 @@ from threading import Lock
from typing import Optional
import click
from configobj import ConfigObj
from libretime_api_client.version1 import AirtimeApiClient as ApiClient
from libretime_shared.cli import cli_logging_options
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 . import pure
from .config import CACHE_DIR, RECORD_DIR
from .config import CACHE_DIR, RECORD_DIR, Config
from .listenerstat import ListenerStat
from .pypofetch import PypoFetch
from .pypofile import PypoFile
@ -81,15 +80,23 @@ def get_liquidsoap_version(version_string):
return None
def liquidsoap_startup_test(telnet_lock, ls_host, ls_port):
def liquidsoap_startup_test(telnet_lock, liquidsoap_host, liquidsoap_port):
liquidsoap_version_string = liquidsoap_get_info(telnet_lock, ls_host, ls_port)
liquidsoap_version_string = liquidsoap_get_info(
telnet_lock,
liquidsoap_host,
liquidsoap_port,
)
while not liquidsoap_version_string:
logger.warning(
"Liquidsoap doesn't appear to be running!, " + "Sleeping and trying again"
)
time.sleep(1)
liquidsoap_version_string = liquidsoap_get_info(telnet_lock, ls_host, ls_port)
liquidsoap_version_string = liquidsoap_get_info(
telnet_lock,
liquidsoap_host,
liquidsoap_port,
)
while pure.version_cmp(liquidsoap_version_string, LIQUIDSOAP_MIN_VERSION) < 0:
logger.warning(
@ -98,24 +105,24 @@ def liquidsoap_startup_test(telnet_lock, ls_host, ls_port):
% LIQUIDSOAP_MIN_VERSION
)
time.sleep(1)
liquidsoap_version_string = liquidsoap_get_info(telnet_lock, ls_host, ls_port)
liquidsoap_version_string = liquidsoap_get_info(
telnet_lock,
liquidsoap_host,
liquidsoap_port,
)
logger.info("Liquidsoap version string found %s" % liquidsoap_version_string)
@click.command()
@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 playout.
"""
setup_logger(level_from_name(log_level), log_filepath)
# loading config file
try:
config = ConfigObj("/etc/airtime/airtime.conf")
except Exception as e:
logger.error("Error loading config file: %s", e)
config = Config(filepath=config_filepath)
try:
for dir_path in [CACHE_DIR, RECORD_DIR]:
@ -154,16 +161,16 @@ def cli(log_level: str, log_filepath: Optional[Path]):
telnet_lock = Lock()
ls_host = config["pypo"]["ls_host"]
ls_port = config["pypo"]["ls_port"]
liquidsoap_host = config.playout.liquidsoap_host
liquidsoap_port = config.playout.liquidsoap_port
liquidsoap_startup_test(telnet_lock, ls_host, ls_port)
liquidsoap_startup_test(telnet_lock, liquidsoap_host, liquidsoap_port)
pypoFetch_q = Queue()
recorder_q = Queue()
pypoPush_q = Queue()
pypo_liquidsoap = PypoLiquidsoap(telnet_lock, ls_host, ls_port)
pypo_liquidsoap = PypoLiquidsoap(telnet_lock, liquidsoap_host, liquidsoap_port)
"""
This queue is shared between pypo-fetch and pypo-file, where pypo-file
@ -174,25 +181,30 @@ def cli(log_level: str, log_filepath: Optional[Path]):
media_q = Queue()
# Pass only the configuration sections needed; PypoMessageHandler only needs rabbitmq settings
pmh = PypoMessageHandler(pypoFetch_q, recorder_q, config["rabbitmq"])
pmh = PypoMessageHandler(pypoFetch_q, recorder_q, config.rabbitmq)
pmh.daemon = True
pmh.start()
pfile = PypoFile(media_q, config["pypo"])
pfile = PypoFile(media_q)
pfile.daemon = True
pfile.start()
pf = PypoFetch(
pypoFetch_q, pypoPush_q, media_q, telnet_lock, pypo_liquidsoap, config["pypo"]
pypoFetch_q,
pypoPush_q,
media_q,
telnet_lock,
pypo_liquidsoap,
config,
)
pf.daemon = True
pf.start()
pp = PypoPush(pypoPush_q, telnet_lock, pypo_liquidsoap, config["pypo"])
pp = PypoPush(pypoPush_q, telnet_lock, pypo_liquidsoap, config)
pp.daemon = True
pp.start()
recorder = Recorder(recorder_q)
recorder = Recorder(recorder_q, config)
recorder.daemon = True
recorder.start()

View File

@ -2,13 +2,13 @@ import copy
import json
import mimetypes
import os
from pathlib import Path
import signal
import subprocess
import sys
import telnetlib
import time
from datetime import datetime
from pathlib import Path
from queue import Empty
from subprocess import PIPE, Popen
from threading import Thread, Timer
@ -18,7 +18,7 @@ from libretime_api_client import version2 as api_client
from loguru import logger
from . import pure
from .config import CACHE_DIR
from .config import CACHE_DIR, Config
from .timeout import ls_timeout
@ -34,7 +34,13 @@ POLL_INTERVAL = 400
class PypoFetch(Thread):
def __init__(
self, pypoFetch_q, pypoPush_q, media_q, telnet_lock, pypo_liquidsoap, config
self,
pypoFetch_q,
pypoPush_q,
media_q,
telnet_lock,
pypo_liquidsoap,
config: Config,
):
Thread.__init__(self)
@ -186,7 +192,8 @@ class PypoFetch(Thread):
while True:
try:
tn = telnetlib.Telnet(
self.config["ls_host"], self.config["ls_port"]
self.config.playout.liquidsoap_host,
self.config.playout.liquidsoap_port,
)
tn.write("exit\n".encode("utf-8"))
tn.read_all()
@ -219,7 +226,10 @@ class PypoFetch(Thread):
try:
self.telnet_lock.acquire()
tn = telnetlib.Telnet(self.config["ls_host"], self.config["ls_port"])
tn = telnetlib.Telnet(
self.config.playout.liquidsoap_host,
self.config.playout.liquidsoap_port,
)
# update the boot up time of Liquidsoap. Since Liquidsoap is not restarting,
# we are manually adjusting the bootup time variable so the status msg will get
# updated.
@ -266,7 +276,10 @@ class PypoFetch(Thread):
# TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!!
try:
self.telnet_lock.acquire()
tn = telnetlib.Telnet(self.config["ls_host"], self.config["ls_port"])
tn = telnetlib.Telnet(
self.config.playout.liquidsoap_host,
self.config.playout.liquidsoap_port,
)
command = ("vars.stream_metadata_type %s\n" % stream_format).encode("utf-8")
logger.info(command)
tn.write(command)
@ -283,7 +296,10 @@ class PypoFetch(Thread):
# TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!!
try:
self.telnet_lock.acquire()
tn = telnetlib.Telnet(self.config["ls_host"], self.config["ls_port"])
tn = telnetlib.Telnet(
self.config.playout.liquidsoap_host,
self.config.playout.liquidsoap_port,
)
command = ("vars.default_dj_fade %s\n" % fade).encode("utf-8")
logger.info(command)
tn.write(command)
@ -301,7 +317,10 @@ class PypoFetch(Thread):
try:
try:
self.telnet_lock.acquire()
tn = telnetlib.Telnet(self.config["ls_host"], self.config["ls_port"])
tn = telnetlib.Telnet(
self.config.playout.liquidsoap_host,
self.config.playout.liquidsoap_port,
)
command = ("vars.station_name %s\n" % station_name).encode("utf-8")
logger.info(command)
tn.write(command)

View File

@ -1,13 +1,10 @@
import configparser
import hashlib
import json
import os
import shutil
import stat
import sys
import time
import traceback
from configparser import NoOptionError
from queue import Empty
from threading import Thread
@ -16,15 +13,12 @@ from libretime_api_client import version2 as api_client
from loguru import logger
from requests.exceptions import ConnectionError, HTTPError, Timeout
CONFIG_PATH = "/etc/airtime/airtime.conf"
class PypoFile(Thread):
def __init__(self, schedule_queue, config):
def __init__(self, schedule_queue):
Thread.__init__(self)
self.media_queue = schedule_queue
self.media = None
self._config = self.read_config_file(CONFIG_PATH)
self.api_client = api_client.AirtimeApiClient()
def copy_file(self, media_item):
@ -158,19 +152,6 @@ class PypoFile(Thread):
return media_item
def read_config_file(self, config_path):
"""Parse the application's config file located at config_path."""
config = configparser.SafeConfigParser(allow_no_value=True)
try:
config.readfp(open(config_path))
except IOError as e:
logger.debug(
"Failed to open config file at %s: %s" % (config_path, e.strerror)
)
sys.exit()
return config
def main(self):
while True:
try:

View File

@ -12,6 +12,7 @@ from kombu.connection import Connection
from kombu.messaging import Exchange, Queue
from kombu.mixins import ConsumerMixin
from kombu.simple import SimpleQueue
from libretime_shared.config import RabbitMQConfig
from loguru import logger
@ -32,7 +33,7 @@ class RabbitConsumer(ConsumerMixin):
class PypoMessageHandler(Thread):
def __init__(self, pq, rq, config):
def __init__(self, pq, rq, config: RabbitMQConfig):
Thread.__init__(self)
self.pypo_queue = pq
self.recorder_queue = rq
@ -46,10 +47,9 @@ class PypoMessageHandler(Thread):
)
schedule_queue = Queue("pypo-fetch", exchange=schedule_exchange, key="foo")
with Connection(
self.config["host"],
self.config["user"],
self.config["password"],
self.config["vhost"],
f"amqp://{self.config.user}:{self.config.password}"
f"@{self.config.host}:{self.config.port}"
f"/{self.config.vhost}",
heartbeat=5,
) as connection:
rabbit = RabbitConsumer(connection, [schedule_queue], self)

View File

@ -9,10 +9,10 @@ from datetime import datetime, timedelta
from queue import Empty, Queue
from threading import Thread
from configobj import ConfigObj
from libretime_api_client import version1 as api_client
from loguru import logger
from .config import Config
from .pypofetch import PypoFetch
from .pypoliqqueue import PypoLiqQueue
from .timeout import ls_timeout
@ -29,7 +29,7 @@ def is_file(media_item):
class PypoPush(Thread):
def __init__(self, q, telnet_lock, pypo_liquidsoap, config):
def __init__(self, q, telnet_lock, pypo_liquidsoap, config: Config):
Thread.__init__(self)
self.api_client = api_client.AirtimeApiClient()
self.queue = q
@ -119,7 +119,10 @@ class PypoPush(Thread):
def stop_web_stream_all(self):
try:
self.telnet_lock.acquire()
tn = telnetlib.Telnet(self.config["LS_HOST"], self.config["LS_PORT"])
tn = telnetlib.Telnet(
self.config.playout.liquidsoap_host,
self.config.playout.liquidsoap_port,
)
# msg = 'dynamic_source.read_stop_all xxx\n'
msg = "http.stop\n"

View File

@ -12,11 +12,10 @@ from threading import Thread
import mutagen
import pytz
from configobj import ConfigObj
from libretime_api_client.version1 import AirtimeApiClient as AirtimeApiClientV1
from loguru import logger
from libretime_playout.config import RECORD_DIR
from libretime_playout.config import RECORD_DIR, Config
def api_client():
@ -27,13 +26,6 @@ def api_client():
return AirtimeApiClientV1()
# loading config file
try:
config = ConfigObj("/etc/airtime/airtime.conf")
except Exception as e:
print("Error loading config file: {}".format(e))
sys.exit()
# TODO : add docstrings everywhere in this module
@ -57,9 +49,17 @@ PUSH_INTERVAL = 2
class ShowRecorder(Thread):
def __init__(self, show_instance, show_name, filelength, start_time):
def __init__(
self,
show_instance,
show_name,
filelength,
start_time,
config: Config,
):
Thread.__init__(self)
self.api_client = api_client()
self.config = config
self.filelength = filelength
self.start_time = start_time
self.show_instance = show_instance
@ -71,18 +71,13 @@ class ShowRecorder(Thread):
filename = self.start_time
filename = filename.replace(" ", "-")
if config["pypo"]["record_file_type"] in ["mp3", "ogg"]:
filetype = config["pypo"]["record_file_type"]
else:
filetype = "ogg"
joined_path = os.path.join(RECORD_DIR, filename)
filepath = "%s.%s" % (joined_path, filetype)
filepath = "%s.%s" % (joined_path, self.config.playout.record_file_format)
br = config["pypo"]["record_bitrate"]
sr = config["pypo"]["record_samplerate"]
c = config["pypo"]["record_channels"]
ss = config["pypo"]["record_sample_size"]
br = self.config.playout.record_bitrate
sr = self.config.playout.record_samplerate
c = self.config.playout.record_channels
ss = self.config.playout.record_sample_size
# -f:16,2,44100
# -b:256
@ -183,9 +178,10 @@ class ShowRecorder(Thread):
class Recorder(Thread):
def __init__(self, q):
def __init__(self, q, config: Config):
Thread.__init__(self)
self.api_client = api_client()
self.config = config
self.sr = None
self.shows_to_record = {}
self.server_timezone = ""
@ -316,6 +312,7 @@ class Recorder(Thread):
show_name,
show_length_seconds,
start_time_formatted,
self.config,
)
self.sr.start()
break

View File

@ -42,6 +42,7 @@ setup(
"packaging",
"pytz",
"requests",
"typing-extensions",
],
extras_require={
"dev": [