2020-03-19 19:41:22 +00:00
|
|
|
<?php
|
|
|
|
namespace App\Radio\Backend\Liquidsoap;
|
|
|
|
|
|
|
|
use App\Entity;
|
|
|
|
use App\Event\Radio\WriteLiquidsoapConfiguration;
|
|
|
|
use App\Exception;
|
|
|
|
use App\Logger;
|
|
|
|
use App\Message;
|
|
|
|
use App\Radio\Adapters;
|
|
|
|
use App\Radio\Backend\Liquidsoap;
|
|
|
|
use App\Settings;
|
2020-06-26 20:22:53 +00:00
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
2020-03-19 19:41:22 +00:00
|
|
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
|
|
|
|
|
|
class ConfigWriter implements EventSubscriberInterface
|
|
|
|
{
|
|
|
|
public const CUSTOM_TOP = 'custom_config_top';
|
|
|
|
public const CUSTOM_PRE_PLAYLISTS = 'custom_config_pre_playlists';
|
|
|
|
public const CUSTOM_PRE_BROADCAST = 'custom_config';
|
|
|
|
public const CUSTOM_PRE_LIVE = 'custom_config_pre_live';
|
|
|
|
public const CUSTOM_PRE_FADE = 'custom_config_pre_fade';
|
|
|
|
|
|
|
|
public const CROSSFADE_NORMAL = 'normal';
|
|
|
|
public const CROSSFADE_DISABLED = 'none';
|
|
|
|
public const CROSSFADE_SMART = 'smart';
|
|
|
|
|
2020-06-26 20:22:53 +00:00
|
|
|
protected EntityManagerInterface $em;
|
2020-03-19 19:41:22 +00:00
|
|
|
|
|
|
|
protected Liquidsoap $liquidsoap;
|
|
|
|
|
2020-06-26 20:22:53 +00:00
|
|
|
public function __construct(EntityManagerInterface $em, Liquidsoap $liquidsoap)
|
2020-03-19 19:41:22 +00:00
|
|
|
{
|
|
|
|
$this->em = $em;
|
|
|
|
$this->liquidsoap = $liquidsoap;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle event dispatch.
|
|
|
|
*
|
|
|
|
* @param Message\AbstractMessage $message
|
|
|
|
*/
|
|
|
|
public function __invoke(Message\AbstractMessage $message)
|
|
|
|
{
|
2020-06-28 23:00:00 +00:00
|
|
|
if ($message instanceof Message\WritePlaylistFileMessage) {
|
|
|
|
$playlist = $this->em->find(Entity\StationPlaylist::class, $message->playlist_id);
|
2020-03-19 19:41:22 +00:00
|
|
|
|
2020-06-28 23:00:00 +00:00
|
|
|
if ($playlist instanceof Entity\StationPlaylist) {
|
|
|
|
$this->writePlaylistFile($playlist, true);
|
2020-03-19 19:41:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function getSubscribedEvents()
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
WriteLiquidsoapConfiguration::class => [
|
|
|
|
['writeHeaderFunctions', 35],
|
|
|
|
['writePlaylistConfiguration', 30],
|
|
|
|
['writeCrossfadeConfiguration', 25],
|
|
|
|
['writeHarborConfiguration', 20],
|
|
|
|
['writePreBroadcastConfiguration', 10],
|
|
|
|
['writeLocalBroadcastConfiguration', 5],
|
|
|
|
['writeRemoteBroadcastConfiguration', 0],
|
|
|
|
],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function writeCustomConfigurationSection(WriteLiquidsoapConfiguration $event, string $sectionName): void
|
|
|
|
{
|
|
|
|
if ($event->isForEditing()) {
|
2020-06-10 00:28:16 +00:00
|
|
|
$divider = self::getDividerString();
|
2020-03-19 19:41:22 +00:00
|
|
|
$event->appendLines([
|
2020-06-10 00:28:16 +00:00
|
|
|
$divider . $sectionName . $divider,
|
2020-03-19 19:41:22 +00:00
|
|
|
]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-27 07:36:30 +00:00
|
|
|
$appSettings = Settings::getInstance();
|
|
|
|
if (!$appSettings->enableAdvancedFeatures()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-19 19:41:22 +00:00
|
|
|
$station = $event->getStation();
|
2020-05-02 07:58:59 +00:00
|
|
|
$settings = $station->getBackendConfig();
|
2020-03-19 19:41:22 +00:00
|
|
|
|
|
|
|
if (!empty($settings[$sectionName])) {
|
|
|
|
$event->appendLines([
|
|
|
|
'# Custom Configuration (Specified in Station Profile)',
|
|
|
|
$settings[$sectionName],
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-10 00:28:16 +00:00
|
|
|
public static function getDividerString(): string
|
|
|
|
{
|
|
|
|
return chr(7);
|
|
|
|
}
|
|
|
|
|
2020-03-19 19:41:22 +00:00
|
|
|
public function writeHeaderFunctions(WriteLiquidsoapConfiguration $event): void
|
|
|
|
{
|
|
|
|
if ($event->isForEditing()) {
|
|
|
|
$event->prependLines([
|
|
|
|
'# ' . __('Welcome to the AzuraCast Liquidsoap configuration editor.'),
|
|
|
|
'# ' . __('Using this page, you can customize several sections of the Liquidsoap configuration.'),
|
|
|
|
'# ' . __('The non-editable sections are automatically generated by AzuraCast.'),
|
|
|
|
]);
|
|
|
|
} else {
|
|
|
|
$event->prependLines([
|
|
|
|
'# WARNING! This file is automatically generated by AzuraCast.',
|
|
|
|
'# Do not update it directly!',
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->writeCustomConfigurationSection($event, self::CUSTOM_TOP);
|
|
|
|
|
|
|
|
$station = $event->getStation();
|
|
|
|
$config_path = $station->getRadioConfigDir();
|
|
|
|
|
|
|
|
$event->appendLines([
|
|
|
|
'set("init.daemon", false)',
|
|
|
|
'set("init.daemon.pidfile.path","' . $config_path . '/liquidsoap.pid")',
|
|
|
|
'set("log.stdout", true)',
|
|
|
|
'set("log.file", false)',
|
|
|
|
'set("server.telnet",true)',
|
|
|
|
'set("server.telnet.bind_addr","' . (Settings::getInstance()->isDocker() ? '0.0.0.0' : '127.0.0.1') . '")',
|
|
|
|
'set("server.telnet.port", ' . $this->liquidsoap->getTelnetPort($station) . ')',
|
|
|
|
'set("harbor.bind_addrs",["0.0.0.0"])',
|
|
|
|
'',
|
|
|
|
'set("tag.encodings",["UTF-8","ISO-8859-1"])',
|
|
|
|
'set("encoder.encoder.export",["artist","title","album","song"])',
|
|
|
|
'',
|
|
|
|
'setenv("TZ", "' . self::cleanUpString($station->getTimezone()) . '")',
|
|
|
|
'',
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function writePlaylistConfiguration(WriteLiquidsoapConfiguration $event): void
|
|
|
|
{
|
|
|
|
$station = $event->getStation();
|
|
|
|
|
|
|
|
$this->writeCustomConfigurationSection($event, self::CUSTOM_PRE_PLAYLISTS);
|
|
|
|
|
|
|
|
// Clear out existing playlists directory.
|
|
|
|
$playlistPath = $station->getRadioPlaylistsDir();
|
|
|
|
$currentPlaylists = array_diff(scandir($playlistPath, SCANDIR_SORT_NONE), ['..', '.']);
|
|
|
|
foreach ($currentPlaylists as $list) {
|
|
|
|
@unlink($playlistPath . '/' . $list);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set up playlists using older format as a fallback.
|
|
|
|
$hasDefaultPlaylist = false;
|
|
|
|
$playlistObjects = [];
|
|
|
|
|
|
|
|
foreach ($station->getPlaylists() as $playlistRaw) {
|
|
|
|
/** @var Entity\StationPlaylist $playlistRaw */
|
|
|
|
if (!$playlistRaw->getIsEnabled()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ($playlistRaw->getType() === Entity\StationPlaylist::TYPE_DEFAULT) {
|
|
|
|
$hasDefaultPlaylist = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$playlistObjects[] = $playlistRaw;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a new default playlist if one doesn't exist.
|
|
|
|
if (!$hasDefaultPlaylist) {
|
|
|
|
Logger::getInstance()->info('No default playlist existed for this station; new one was automatically created.',
|
|
|
|
['station_id' => $station->getId(), 'station_name' => $station->getName()]);
|
|
|
|
|
|
|
|
// Auto-create an empty default playlist.
|
|
|
|
$defaultPlaylist = new Entity\StationPlaylist($station);
|
|
|
|
$defaultPlaylist->setName('default');
|
2020-06-28 23:00:00 +00:00
|
|
|
|
2020-03-19 19:41:22 +00:00
|
|
|
$this->em->persist($defaultPlaylist);
|
|
|
|
$this->em->flush();
|
|
|
|
|
|
|
|
$playlistObjects[] = $defaultPlaylist;
|
|
|
|
}
|
|
|
|
|
|
|
|
$playlistVarNames = [];
|
|
|
|
|
|
|
|
$genPlaylistWeights = [];
|
|
|
|
$genPlaylistVars = [];
|
|
|
|
|
|
|
|
$specialPlaylists = [
|
|
|
|
'once_per_x_songs' => [
|
|
|
|
'# Once per x Songs Playlists',
|
|
|
|
],
|
|
|
|
'once_per_x_minutes' => [
|
|
|
|
'# Once per x Minutes Playlists',
|
|
|
|
],
|
|
|
|
];
|
|
|
|
|
|
|
|
$scheduleSwitches = [];
|
|
|
|
$scheduleSwitchesInterrupting = [];
|
|
|
|
|
|
|
|
foreach ($playlistObjects as $playlist) {
|
|
|
|
/** @var Entity\StationPlaylist $playlist */
|
|
|
|
$playlistVarName = 'playlist_' . str_replace('-', '_', $playlist->getShortName());
|
|
|
|
|
|
|
|
if (in_array($playlistVarName, $playlistVarNames, true)) {
|
|
|
|
$playlistVarName .= '_' . $playlist->getId();
|
|
|
|
}
|
|
|
|
|
|
|
|
$playlistVarNames[] = $playlistVarName;
|
|
|
|
|
|
|
|
$usesRandom = true;
|
|
|
|
$usesReloadMode = true;
|
|
|
|
$usesConservative = false;
|
|
|
|
|
|
|
|
if ($playlist->backendLoopPlaylistOnce()) {
|
|
|
|
$playlistFuncName = 'playlist.once';
|
|
|
|
} elseif ($playlist->backendMerge()) {
|
|
|
|
$playlistFuncName = 'playlist.merge';
|
|
|
|
$usesReloadMode = false;
|
|
|
|
} else {
|
|
|
|
$playlistFuncName = 'playlist';
|
|
|
|
$usesRandom = false;
|
|
|
|
$usesConservative = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$playlistConfigLines = [];
|
|
|
|
|
|
|
|
if (Entity\StationPlaylist::SOURCE_SONGS === $playlist->getSource()) {
|
|
|
|
$playlistFilePath = $this->writePlaylistFile($playlist, false);
|
|
|
|
|
|
|
|
if (!$playlistFilePath) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Liquidsoap's playlist functions support very different argument patterns. :/
|
|
|
|
$playlistParams = [
|
|
|
|
'id="' . self::cleanUpString($playlistVarName) . '"',
|
|
|
|
];
|
|
|
|
|
|
|
|
if ($usesRandom) {
|
|
|
|
if (Entity\StationPlaylist::ORDER_SEQUENTIAL !== $playlist->getOrder()) {
|
|
|
|
$playlistParams[] = 'random=true';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$playlistModes = [
|
|
|
|
Entity\StationPlaylist::ORDER_SEQUENTIAL => 'normal',
|
|
|
|
Entity\StationPlaylist::ORDER_SHUFFLE => 'randomize',
|
|
|
|
Entity\StationPlaylist::ORDER_RANDOM => 'random',
|
|
|
|
];
|
|
|
|
|
|
|
|
$playlistParams[] = 'mode="' . $playlistModes[$playlist->getOrder()] . '"';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($usesReloadMode) {
|
|
|
|
$playlistParams[] = 'reload_mode="watch"';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($usesConservative) {
|
|
|
|
$playlistParams[] = 'conservative=true';
|
|
|
|
$playlistParams[] = 'default_duration=10.';
|
|
|
|
$playlistParams[] = 'length=20.';
|
|
|
|
}
|
|
|
|
|
|
|
|
$playlistParams[] = '"' . $playlistFilePath . '"';
|
|
|
|
|
|
|
|
$playlistConfigLines[] = $playlistVarName . ' = ' . $playlistFuncName . '(' . implode(',',
|
|
|
|
$playlistParams) . ')';
|
|
|
|
} else {
|
|
|
|
switch ($playlist->getRemoteType()) {
|
|
|
|
case Entity\StationPlaylist::REMOTE_TYPE_PLAYLIST:
|
|
|
|
$playlistFunc = $playlistFuncName . '("' . self::cleanUpString($playlist->getRemoteUrl()) . '")';
|
|
|
|
$playlistConfigLines[] = $playlistVarName . ' = ' . $playlistFunc;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Entity\StationPlaylist::REMOTE_TYPE_STREAM:
|
|
|
|
default:
|
|
|
|
$remote_url = $playlist->getRemoteUrl();
|
|
|
|
$remote_url_scheme = parse_url($remote_url, PHP_URL_SCHEME);
|
|
|
|
$remote_url_function = ('https' === $remote_url_scheme) ? 'input.https' : 'input.http';
|
|
|
|
|
|
|
|
$buffer = $playlist->getRemoteBuffer();
|
|
|
|
$buffer = ($buffer < 1) ? Entity\StationPlaylist::DEFAULT_REMOTE_BUFFER : $buffer;
|
|
|
|
|
|
|
|
$playlistConfigLines[] = $playlistVarName . ' = mksafe(' . $remote_url_function . '(max=' . $buffer . '., "' . self::cleanUpString($remote_url) . '"))';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$playlistConfigLines[] = $playlistVarName . ' = audio_to_stereo(id="stereo_' . self::cleanUpString($playlistVarName) . '", ' . $playlistVarName . ')';
|
|
|
|
$playlistConfigLines[] = $playlistVarName . ' = cue_cut(id="cue_' . self::cleanUpString($playlistVarName) . '", ' . $playlistVarName . ')';
|
|
|
|
|
|
|
|
if ($playlist->isJingle()) {
|
|
|
|
$playlistConfigLines[] = $playlistVarName . ' = drop_metadata(' . $playlistVarName . ')';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Entity\StationPlaylist::TYPE_ADVANCED === $playlist->getType()) {
|
|
|
|
$playlistConfigLines[] = 'ignore(' . $playlistVarName . ')';
|
|
|
|
}
|
|
|
|
|
|
|
|
$event->appendLines($playlistConfigLines);
|
|
|
|
|
|
|
|
if ($playlist->backendPlaySingleTrack()) {
|
|
|
|
$playlistVarName = 'once(' . $playlistVarName . ')';
|
|
|
|
}
|
|
|
|
|
|
|
|
$scheduleItems = $playlist->getScheduleItems();
|
|
|
|
|
|
|
|
switch ($playlist->getType()) {
|
|
|
|
case Entity\StationPlaylist::TYPE_DEFAULT:
|
|
|
|
if ($scheduleItems->count() > 0) {
|
|
|
|
foreach ($scheduleItems as $scheduleItem) {
|
|
|
|
$play_time = $this->getScheduledPlaylistPlayTime($scheduleItem);
|
|
|
|
|
|
|
|
$schedule_timing = '({ ' . $play_time . ' }, ' . $playlistVarName . ')';
|
|
|
|
if ($playlist->backendInterruptOtherSongs()) {
|
|
|
|
$scheduleSwitchesInterrupting[] = $schedule_timing;
|
|
|
|
} else {
|
|
|
|
$scheduleSwitches[] = $schedule_timing;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$genPlaylistWeights[] = $playlist->getWeight();
|
|
|
|
$genPlaylistVars[] = $playlistVarName;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Entity\StationPlaylist::TYPE_ONCE_PER_X_SONGS:
|
|
|
|
case Entity\StationPlaylist::TYPE_ONCE_PER_X_MINUTES:
|
|
|
|
if (Entity\StationPlaylist::TYPE_ONCE_PER_X_SONGS === $playlist->getType()) {
|
|
|
|
$playlistScheduleVar = 'rotate(weights=[1,' . $playlist->getPlayPerSongs() . '], [' . $playlistVarName . ', radio])';
|
|
|
|
} else {
|
|
|
|
$delaySeconds = $playlist->getPlayPerMinutes() * 60;
|
|
|
|
$delayTrackSensitive = $playlist->backendInterruptOtherSongs() ? 'false' : 'true';
|
|
|
|
|
|
|
|
$playlistScheduleVar = 'fallback(track_sensitive=' . $delayTrackSensitive . ', [delay(' . $delaySeconds . '., ' . $playlistVarName . '), radio])';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($scheduleItems->count() > 0) {
|
|
|
|
foreach ($scheduleItems as $scheduleItem) {
|
|
|
|
$play_time = $this->getScheduledPlaylistPlayTime($scheduleItem);
|
|
|
|
|
|
|
|
$schedule_timing = '({ ' . $play_time . ' }, ' . $playlistScheduleVar . ')';
|
|
|
|
if ($playlist->backendInterruptOtherSongs()) {
|
|
|
|
$scheduleSwitchesInterrupting[] = $schedule_timing;
|
|
|
|
} else {
|
|
|
|
$scheduleSwitches[] = $schedule_timing;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$specialPlaylists[$playlist->getType()][] = 'radio = ' . $playlistScheduleVar;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Entity\StationPlaylist::TYPE_ONCE_PER_HOUR:
|
|
|
|
$minutePlayTime = $playlist->getPlayPerHourMinute() . 'm';
|
|
|
|
|
|
|
|
if ($scheduleItems->count() > 0) {
|
|
|
|
foreach ($scheduleItems as $scheduleItem) {
|
|
|
|
$playTime = '(' . $minutePlayTime . ') and (' . $this->getScheduledPlaylistPlayTime($scheduleItem) . ')';
|
|
|
|
|
|
|
|
$schedule_timing = '({ ' . $playTime . ' }, ' . $playlistVarName . ')';
|
|
|
|
if ($playlist->backendInterruptOtherSongs()) {
|
|
|
|
$scheduleSwitchesInterrupting[] = $schedule_timing;
|
|
|
|
} else {
|
|
|
|
$scheduleSwitches[] = $schedule_timing;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$schedule_timing = '({ ' . $minutePlayTime . ' }, ' . $playlistVarName . ')';
|
|
|
|
if ($playlist->backendInterruptOtherSongs()) {
|
|
|
|
$scheduleSwitchesInterrupting[] = $schedule_timing;
|
|
|
|
} else {
|
|
|
|
$scheduleSwitches[] = $schedule_timing;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build "default" type playlists.
|
|
|
|
$event->appendLines([
|
|
|
|
'# Standard Playlists',
|
2020-03-22 00:29:07 +00:00
|
|
|
'radio = random(id="' . self::getVarName($station, 'standard_playlists') . '", weights=[' . implode(', ',
|
2020-03-19 19:41:22 +00:00
|
|
|
$genPlaylistWeights) . '], [' . implode(', ', $genPlaylistVars) . '])',
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (!empty($scheduleSwitches)) {
|
|
|
|
$scheduleSwitches[] = '({true}, radio)';
|
|
|
|
|
|
|
|
$event->appendLines([
|
|
|
|
'# Standard Schedule Switches',
|
2020-03-22 00:29:07 +00:00
|
|
|
'radio = switch(id="' . self::getVarName($station,
|
|
|
|
'schedule_switch') . '", track_sensitive=true, [ ' . implode(', ', $scheduleSwitches) . ' ])',
|
2020-03-19 19:41:22 +00:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add in special playlists if necessary.
|
|
|
|
foreach ($specialPlaylists as $playlist_type => $playlistConfigLines) {
|
|
|
|
if (count($playlistConfigLines) > 1) {
|
|
|
|
$event->appendLines($playlistConfigLines);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$station->useManualAutoDJ()) {
|
|
|
|
|
|
|
|
$nextsongCommand = $this->getApiUrlCommand($station, 'nextsong');
|
|
|
|
|
|
|
|
$event->appendBlock(<<< EOF
|
|
|
|
# AutoDJ Next Song Script
|
|
|
|
def azuracast_next_song() =
|
|
|
|
uri = {$nextsongCommand}
|
|
|
|
log("AzuraCast Raw Response: #{uri}")
|
|
|
|
|
|
|
|
if uri == "" or string.match(pattern="Error", uri) then
|
2020-05-08 06:46:30 +00:00
|
|
|
[]
|
|
|
|
else
|
|
|
|
req = request.create(uri)
|
|
|
|
[req]
|
2020-03-19 19:41:22 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
EOF
|
|
|
|
);
|
|
|
|
|
|
|
|
$event->appendLines([
|
2020-05-08 06:46:30 +00:00
|
|
|
'dynamic = request.dynamic.list(id="' . self::getVarName($station,
|
|
|
|
'next_song') . '", timeout=20., retry_delay=3., azuracast_next_song)',
|
2020-03-22 00:29:07 +00:00
|
|
|
'dynamic = audio_to_stereo(id="' . self::getVarName($station, 'stereo_next_song') . '", dynamic)',
|
|
|
|
'dynamic = cue_cut(id="' . self::getVarName($station, 'cue_next_song') . '", dynamic)',
|
|
|
|
|
|
|
|
'radio = fallback(id="' . self::getVarName($station,
|
|
|
|
'autodj_fallback') . '", track_sensitive = true, [dynamic, radio])',
|
2020-03-19 19:41:22 +00:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!empty($scheduleSwitchesInterrupting)) {
|
|
|
|
$scheduleSwitchesInterrupting[] = '({true}, radio)';
|
|
|
|
|
|
|
|
$event->appendLines([
|
|
|
|
'# Interrupting Schedule Switches',
|
2020-03-22 00:29:07 +00:00
|
|
|
'radio = switch(id="' . self::getVarName($station,
|
|
|
|
'interrupt_switch') . '", track_sensitive=false, [ ' . implode(', ',
|
|
|
|
$scheduleSwitchesInterrupting) . ' ])',
|
2020-03-19 19:41:22 +00:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$event->appendLines([
|
2020-03-22 00:29:07 +00:00
|
|
|
'requests = request.queue(id="' . self::getVarName($station, 'requests') . '")',
|
|
|
|
'requests = audio_to_stereo(id="' . self::getVarName($station, 'stereo_requests') . '", requests)',
|
|
|
|
'requests = cue_cut(id="' . self::getVarName($station, 'cue_requests') . '", requests)',
|
2020-03-19 19:41:22 +00:00
|
|
|
|
2020-03-22 00:29:07 +00:00
|
|
|
'radio = fallback(id="' . self::getVarName($station,
|
|
|
|
'requests_fallback') . '", track_sensitive = true, [requests, radio])',
|
2020-03-19 19:41:22 +00:00
|
|
|
'',
|
|
|
|
'add_skip_command(radio)',
|
|
|
|
'',
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Write a playlist's contents to file so Liquidsoap can process it, and optionally notify
|
|
|
|
* Liquidsoap of the change.
|
|
|
|
*
|
|
|
|
* @param Entity\StationPlaylist $playlist
|
|
|
|
* @param bool $notify
|
|
|
|
*
|
|
|
|
* @return string The full path that was written to.
|
|
|
|
*/
|
|
|
|
public function writePlaylistFile(Entity\StationPlaylist $playlist, $notify = true): ?string
|
|
|
|
{
|
|
|
|
$station = $playlist->getStation();
|
|
|
|
|
|
|
|
$playlistPath = $station->getRadioPlaylistsDir();
|
|
|
|
$playlistVarName = 'playlist_' . $playlist->getShortName();
|
|
|
|
|
|
|
|
$logger = Logger::getInstance();
|
|
|
|
$logger->info('Writing playlist file to disk...', [
|
|
|
|
'station' => $station->getName(),
|
|
|
|
'playlist' => $playlist->getName(),
|
|
|
|
]);
|
|
|
|
|
|
|
|
$mediaBaseDir = $station->getRadioMediaDir() . '/';
|
|
|
|
$playlistFile = [];
|
2020-06-26 20:22:53 +00:00
|
|
|
|
2020-03-19 19:41:22 +00:00
|
|
|
$mediaQuery = $this->em->createQuery(/** @lang DQL */ 'SELECT DISTINCT sm
|
|
|
|
FROM App\Entity\StationMedia sm
|
|
|
|
JOIN sm.playlists spm
|
|
|
|
WHERE spm.playlist = :playlist
|
|
|
|
ORDER BY spm.weight ASC
|
|
|
|
')->setParameter('playlist', $playlist);
|
|
|
|
|
|
|
|
$mediaIterator = $mediaQuery->iterate();
|
|
|
|
|
|
|
|
foreach ($mediaIterator as $row) {
|
|
|
|
/** @var Entity\StationMedia $mediaFile */
|
|
|
|
$mediaFile = $row[0];
|
|
|
|
|
|
|
|
$mediaFilePath = $mediaBaseDir . $mediaFile->getPath();
|
2020-04-11 06:55:00 +00:00
|
|
|
$mediaAnnotations = $this->liquidsoap->annotateMedia($mediaFile);
|
2020-03-19 19:41:22 +00:00
|
|
|
|
|
|
|
if ($playlist->isJingle()) {
|
|
|
|
$mediaAnnotations['is_jingle_mode'] = 'true';
|
|
|
|
unset($mediaAnnotations['media_id']);
|
|
|
|
} else {
|
|
|
|
$mediaAnnotations['playlist_id'] = $playlist->getId();
|
|
|
|
}
|
|
|
|
|
|
|
|
$annotations_str = [];
|
|
|
|
foreach ($mediaAnnotations as $annotation_key => $annotation_val) {
|
|
|
|
if ('liq_amplify' === $annotation_key) {
|
|
|
|
$annotations_str[] = $annotation_key . '="' . $annotation_val . 'dB"';
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$annotations_str[] = $annotation_key . '="' . $annotation_val . '"';
|
|
|
|
}
|
|
|
|
|
|
|
|
$playlistFile[] = 'annotate:' . implode(',', $annotations_str) . ':' . $mediaFilePath;
|
|
|
|
|
|
|
|
$this->em->detach($mediaFile);
|
|
|
|
unset($mediaFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
$playlistFilePath = $playlistPath . '/' . $playlistVarName . '.m3u';
|
|
|
|
|
|
|
|
file_put_contents($playlistFilePath, implode("\n", $playlistFile));
|
|
|
|
|
|
|
|
if ($notify) {
|
|
|
|
try {
|
|
|
|
$this->liquidsoap->command($station, $playlistVarName . '.reload');
|
|
|
|
} catch (Exception $e) {
|
|
|
|
Logger::getInstance()->error('Could not reload playlist with AutoDJ.', [
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
'playlist' => $playlistVarName,
|
|
|
|
'station' => $station->getId(),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $playlistFilePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a scheduled playlist, return the time criteria that Liquidsoap can use to determine when to play it.
|
|
|
|
*
|
|
|
|
* @param Entity\StationSchedule $playlistSchedule
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function getScheduledPlaylistPlayTime(Entity\StationSchedule $playlistSchedule): string
|
|
|
|
{
|
|
|
|
$start_time = $playlistSchedule->getStartTime();
|
|
|
|
$end_time = $playlistSchedule->getEndTime();
|
|
|
|
|
|
|
|
// Handle multi-day playlists.
|
|
|
|
if ($start_time > $end_time) {
|
|
|
|
$play_times = [
|
|
|
|
self::formatTimeCode($start_time) . '-23h59m59s',
|
|
|
|
'00h00m-' . self::formatTimeCode($end_time),
|
|
|
|
];
|
|
|
|
|
|
|
|
$playlist_schedule_days = $playlistSchedule->getDays();
|
|
|
|
if (!empty($playlist_schedule_days) && count($playlist_schedule_days) < 7) {
|
|
|
|
$current_play_days = [];
|
|
|
|
$next_play_days = [];
|
|
|
|
|
|
|
|
foreach ($playlist_schedule_days as $day) {
|
|
|
|
$day = (int)$day;
|
|
|
|
$current_play_days[] = (($day === 7) ? '0' : $day) . 'w';
|
|
|
|
|
|
|
|
$day++;
|
|
|
|
if ($day > 7) {
|
|
|
|
$day = 1;
|
|
|
|
}
|
|
|
|
$next_play_days[] = (($day === 7) ? '0' : $day) . 'w';
|
|
|
|
}
|
|
|
|
|
|
|
|
$play_times[0] = '(' . implode(' or ', $current_play_days) . ') and ' . $play_times[0];
|
|
|
|
$play_times[1] = '(' . implode(' or ', $next_play_days) . ') and ' . $play_times[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
return '(' . implode(') or (', $play_times) . ')';
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle once-per-day playlists.
|
|
|
|
$play_time = ($start_time === $end_time)
|
|
|
|
? self::formatTimeCode($start_time)
|
|
|
|
: self::formatTimeCode($start_time) . '-' . self::formatTimeCode($end_time);
|
|
|
|
|
|
|
|
$playlist_schedule_days = $playlistSchedule->getDays();
|
|
|
|
if (!empty($playlist_schedule_days) && count($playlist_schedule_days) < 7) {
|
|
|
|
$play_days = [];
|
|
|
|
|
|
|
|
foreach ($playlist_schedule_days as $day) {
|
|
|
|
$day = (int)$day;
|
|
|
|
$play_days[] = (($day === 7) ? '0' : $day) . 'w';
|
|
|
|
}
|
|
|
|
|
|
|
|
$play_time = '(' . implode(' or ', $play_days) . ') and ' . $play_time;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $play_time;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the URL that LiquidSoap should call when attempting to execute AzuraCast API commands.
|
|
|
|
*
|
|
|
|
* @param Entity\Station $station
|
|
|
|
* @param string $endpoint
|
|
|
|
* @param array $params
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function getApiUrlCommand(Entity\Station $station, $endpoint, $params = []): string
|
|
|
|
{
|
|
|
|
$settings = Settings::getInstance();
|
|
|
|
|
|
|
|
// Docker cURL-based API URL call with API authentication.
|
|
|
|
if ($settings->isDocker()) {
|
|
|
|
$params = (array)$params;
|
|
|
|
$params['api_auth'] = '"' . $station->getAdapterApiKey() . '"';
|
|
|
|
|
|
|
|
$service_uri = ($settings[Settings::DOCKER_REVISION] >= 5) ? 'web' : 'nginx';
|
|
|
|
$api_url = 'http://' . $service_uri . '/api/internal/' . $station->getId() . '/' . $endpoint;
|
|
|
|
$command = 'curl -s --request POST --url ' . $api_url;
|
|
|
|
foreach ($params as $param_key => $param_val) {
|
|
|
|
$command .= ' --form ' . $param_key . '="^string.quote(' . $param_val . ')^"';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Ansible shell-script call.
|
|
|
|
$shell_path = '/usr/bin/php ' . $settings->getBaseDirectory() . '/bin/console';
|
|
|
|
|
|
|
|
$shell_args = [];
|
|
|
|
$shell_args[] = 'azuracast:internal:' . $endpoint;
|
|
|
|
$shell_args[] = $station->getId();
|
|
|
|
|
|
|
|
foreach ((array)$params as $param_key => $param_val) {
|
|
|
|
$shell_args [] = '--' . $param_key . '="^string.quote(' . $param_val . ')^"';
|
|
|
|
}
|
|
|
|
|
|
|
|
$command = $shell_path . ' ' . implode(' ', $shell_args);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 'list.hd(get_process_lines("' . $command . '"), default="")';
|
|
|
|
}
|
|
|
|
|
|
|
|
public function writeCrossfadeConfiguration(WriteLiquidsoapConfiguration $event): void
|
|
|
|
{
|
|
|
|
$station = $event->getStation();
|
2020-05-02 07:58:59 +00:00
|
|
|
$settings = $station->getBackendConfig();
|
2020-03-19 19:41:22 +00:00
|
|
|
|
|
|
|
// Write pre-crossfade section.
|
|
|
|
$this->writeCustomConfigurationSection($event, self::CUSTOM_PRE_FADE);
|
|
|
|
|
|
|
|
// Crossfading happens before the live broadcast is mixed in, because of buffer issues.
|
|
|
|
$crossfade_type = $settings['crossfade_type'] ?? self::CROSSFADE_NORMAL;
|
|
|
|
$crossfade = round($settings['crossfade'] ?? 2, 1);
|
|
|
|
|
|
|
|
if (self::CROSSFADE_DISABLED !== $crossfade_type && $crossfade > 0) {
|
|
|
|
$start_next = round($crossfade * 1.5, 2);
|
|
|
|
$crossfadeIsSmart = (self::CROSSFADE_SMART === $crossfade_type) ? 'true' : 'false';
|
|
|
|
|
|
|
|
$event->appendLines([
|
|
|
|
'radio = crossfade(smart=' . $crossfadeIsSmart . ', duration=' . self::toFloat($start_next) . ',fade_out=' . self::toFloat($crossfade) . ',fade_in=' . self::toFloat($crossfade) . ',radio)',
|
|
|
|
]);
|
|
|
|
}
|
2020-05-03 17:55:42 +00:00
|
|
|
|
|
|
|
// Write fallback to safety file immediately after crossfade.
|
|
|
|
$error_file = Settings::getInstance()->isDocker()
|
|
|
|
? '/usr/local/share/icecast/web/error.mp3'
|
|
|
|
: Settings::getInstance()->getBaseDirectory() . '/resources/error.mp3';
|
|
|
|
|
|
|
|
$event->appendLines([
|
|
|
|
'radio = fallback(id="' . self::getVarName($station,
|
|
|
|
'safe_fallback') . '", track_sensitive = false, [radio, single(id="error_jingle", "' . $error_file . '")])',
|
|
|
|
]);
|
2020-03-19 19:41:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function writeHarborConfiguration(WriteLiquidsoapConfiguration $event): void
|
|
|
|
{
|
|
|
|
$station = $event->getStation();
|
|
|
|
|
|
|
|
if (!$station->getEnableStreamers()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->writeCustomConfigurationSection($event, self::CUSTOM_PRE_LIVE);
|
|
|
|
|
2020-05-02 07:58:59 +00:00
|
|
|
$settings = $station->getBackendConfig();
|
|
|
|
$charset = $settings->getCharset();
|
|
|
|
$dj_mount = $settings->getDjMountPoint();
|
|
|
|
$recordLiveStreams = $settings->recordStreams();
|
2020-03-19 19:41:22 +00:00
|
|
|
|
|
|
|
$authCommand = $this->getApiUrlCommand($station, 'auth', ['dj-user' => '!user', 'dj-password' => '!password']);
|
|
|
|
$djonCommand = $this->getApiUrlCommand($station, 'djon', ['dj-user' => 'dj']);
|
|
|
|
$djoffCommand = $this->getApiUrlCommand($station, 'djoff', ['dj-user' => 'dj']);
|
|
|
|
|
|
|
|
$event->appendBlock(<<< EOF
|
|
|
|
# DJ Authentication
|
|
|
|
live_enabled = ref false
|
|
|
|
last_authenticated_dj = ref ""
|
|
|
|
live_dj = ref ""
|
|
|
|
|
|
|
|
def dj_auth(auth_user,auth_pw) =
|
|
|
|
user = ref ""
|
|
|
|
password = ref ""
|
|
|
|
|
|
|
|
if (auth_user == "source" or auth_user == "") and (string.match(pattern="(:|,)+", auth_pw)) then
|
|
|
|
auth_string = string.split(separator="(:|,)", auth_pw)
|
|
|
|
|
|
|
|
user := list.nth(default="", auth_string, 0)
|
|
|
|
password := list.nth(default="", auth_string, 2)
|
|
|
|
else
|
|
|
|
user := auth_user
|
|
|
|
password := auth_pw
|
|
|
|
end
|
|
|
|
|
|
|
|
log("Authenticating DJ: #{!user}")
|
|
|
|
|
|
|
|
ret = {$authCommand}
|
|
|
|
log("AzuraCast DJ Auth Response: #{ret}")
|
|
|
|
|
|
|
|
authed = bool_of_string(ret)
|
|
|
|
if (authed) then
|
|
|
|
last_authenticated_dj := !user
|
|
|
|
end
|
|
|
|
|
|
|
|
authed
|
|
|
|
end
|
|
|
|
|
|
|
|
def live_connected(header) =
|
|
|
|
dj = !last_authenticated_dj
|
|
|
|
log("DJ Source connected! Last authenticated DJ: #{dj} - #{header}")
|
|
|
|
|
|
|
|
live_enabled := true
|
|
|
|
live_dj := dj
|
|
|
|
|
|
|
|
ret = {$djonCommand}
|
|
|
|
log("AzuraCast Live Connected Response: #{ret}")
|
|
|
|
end
|
|
|
|
|
|
|
|
def live_disconnected() =
|
|
|
|
dj = !live_dj
|
|
|
|
|
|
|
|
log("DJ Source disconnected! Current live DJ: #{dj}")
|
|
|
|
|
|
|
|
ret = {$djoffCommand}
|
|
|
|
log("AzuraCast Live Disconnected Response: #{ret}")
|
|
|
|
|
|
|
|
live_enabled := false
|
|
|
|
last_authenticated_dj := ""
|
|
|
|
live_dj := ""
|
|
|
|
end
|
|
|
|
EOF
|
|
|
|
);
|
|
|
|
|
|
|
|
$harbor_params = [
|
|
|
|
'"' . self::cleanUpString($dj_mount) . '"',
|
2020-03-22 00:29:07 +00:00
|
|
|
'id = "' . self::getVarName($station, 'input_streamer') . '"',
|
2020-03-19 19:41:22 +00:00
|
|
|
'port = ' . $this->liquidsoap->getStreamPort($station),
|
|
|
|
'auth = dj_auth',
|
|
|
|
'icy = true',
|
|
|
|
'icy_metadata_charset = "' . $charset . '"',
|
|
|
|
'metadata_charset = "' . $charset . '"',
|
|
|
|
'on_connect = live_connected',
|
|
|
|
'on_disconnect = live_disconnected',
|
|
|
|
];
|
|
|
|
|
|
|
|
$djBuffer = (int)($settings['dj_buffer'] ?? 5);
|
|
|
|
if (0 !== $djBuffer) {
|
|
|
|
$harbor_params[] = 'buffer = ' . self::toFloat($djBuffer);
|
|
|
|
$harbor_params[] = 'max = ' . self::toFloat(max($djBuffer + 5, 10));
|
|
|
|
}
|
|
|
|
|
|
|
|
$event->appendLines([
|
|
|
|
'# A Pre-DJ source of radio that can be broadcast if needed',
|
|
|
|
'radio_without_live = radio',
|
|
|
|
'ignore(radio_without_live)',
|
|
|
|
'',
|
|
|
|
'# Live Broadcasting',
|
|
|
|
'live = audio_to_stereo(input.harbor(' . implode(', ', $harbor_params) . '))',
|
|
|
|
'ignore(output.dummy(live, fallible=true))',
|
|
|
|
'',
|
2020-03-22 00:29:07 +00:00
|
|
|
'radio = fallback(id="' . self::getVarName($station,
|
2020-04-24 08:43:52 +00:00
|
|
|
'live_fallback') . '", replay_metadata=false, track_sensitive=false, [live, radio])',
|
2020-03-19 19:41:22 +00:00
|
|
|
]);
|
|
|
|
|
|
|
|
if ($recordLiveStreams) {
|
|
|
|
$recordLiveStreamsFormat = $settings['record_streams_format'] ?? Entity\StationMountInterface::FORMAT_MP3;
|
|
|
|
$recordLiveStreamsBitrate = (int)($settings['record_streams_bitrate'] ?? 128);
|
|
|
|
|
|
|
|
$formatString = $this->getOutputFormatString($recordLiveStreamsFormat, $recordLiveStreamsBitrate);
|
|
|
|
|
|
|
|
$event->appendBlock(<<< EOF
|
|
|
|
# Record Live Broadcasts
|
|
|
|
stop_recording_f = ref (fun () -> ())
|
|
|
|
|
|
|
|
def start_recording(path) =
|
|
|
|
output_live_recording = output.file({$formatString}, fallible=true, reopen_on_metadata=false, "#{path}", live)
|
|
|
|
stop_recording_f := fun () -> source.shutdown(output_live_recording)
|
|
|
|
end
|
|
|
|
|
|
|
|
def stop_recording() =
|
|
|
|
f = !stop_recording_f
|
|
|
|
f ()
|
|
|
|
|
|
|
|
stop_recording_f := fun () -> ()
|
|
|
|
end
|
|
|
|
|
|
|
|
server.register(namespace="recording", description="Start recording.", usage="recording.start filename", "start", fun (s) -> begin start_recording(s) "Done!" end)
|
|
|
|
server.register(namespace="recording", description="Stop recording.", usage="recording.stop", "stop", fun (s) -> begin stop_recording() "Done!" end)
|
|
|
|
EOF
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function writePreBroadcastConfiguration(WriteLiquidsoapConfiguration $event): void
|
|
|
|
{
|
|
|
|
$station = $event->getStation();
|
2020-05-02 07:58:59 +00:00
|
|
|
$settings = $station->getBackendConfig();
|
2020-03-19 19:41:22 +00:00
|
|
|
|
|
|
|
$event->appendLines([
|
|
|
|
'# Allow for Telnet-driven insertion of custom metadata.',
|
|
|
|
'radio = server.insert_metadata(id="custom_metadata", radio)',
|
|
|
|
'',
|
|
|
|
'# Apply amplification metadata (if supplied)',
|
|
|
|
'radio = amplify(override="liq_amplify", 1., radio)',
|
|
|
|
]);
|
|
|
|
|
|
|
|
// NRJ normalization
|
2020-05-02 07:58:59 +00:00
|
|
|
if ($settings->useNormalizer()) {
|
2020-03-19 19:41:22 +00:00
|
|
|
$event->appendLines([
|
|
|
|
'# Normalization and Compression',
|
|
|
|
'radio = normalize(target = 0., window = 0.03, gain_min = -16., gain_max = 0., radio)',
|
|
|
|
'radio = compress.exponential(radio, mu = 1.0)',
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Replaygain metadata
|
2020-05-02 07:58:59 +00:00
|
|
|
if ($settings->useReplayGain()) {
|
2020-03-19 19:41:22 +00:00
|
|
|
$event->appendLines([
|
|
|
|
'# Replaygain Metadata',
|
|
|
|
'enable_replaygain_metadata()',
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Custom configuration
|
|
|
|
$this->writeCustomConfigurationSection($event, self::CUSTOM_PRE_BROADCAST);
|
|
|
|
|
|
|
|
$feedbackCommand = $this->getApiUrlCommand($station, 'feedback',
|
|
|
|
['song' => 'm["song_id"]', 'media' => 'm["media_id"]', 'playlist' => 'm["playlist_id"]']);
|
|
|
|
|
|
|
|
$event->appendBlock(<<<EOF
|
|
|
|
# Send metadata changes back to AzuraCast
|
|
|
|
def metadata_updated(m) =
|
|
|
|
if (m["song_id"] != "") then
|
|
|
|
ret = {$feedbackCommand}
|
|
|
|
log("AzuraCast Feedback Response: #{ret}")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
radio = on_metadata(metadata_updated,radio)
|
|
|
|
EOF
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function writeLocalBroadcastConfiguration(WriteLiquidsoapConfiguration $event): void
|
|
|
|
{
|
|
|
|
$station = $event->getStation();
|
|
|
|
|
|
|
|
if (Adapters::FRONTEND_REMOTE === $station->getFrontendType()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$ls_config = [
|
|
|
|
'# Local Broadcasts',
|
|
|
|
];
|
|
|
|
|
|
|
|
// Configure the outbound broadcast.
|
|
|
|
$i = 0;
|
|
|
|
foreach ($station->getMounts() as $mount_row) {
|
|
|
|
$i++;
|
|
|
|
|
|
|
|
/** @var Entity\StationMount $mount_row */
|
|
|
|
if (!$mount_row->getEnableAutodj()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$ls_config[] = $this->getOutputString($station, $mount_row, 'local_', $i);
|
|
|
|
}
|
|
|
|
|
|
|
|
$event->appendLines($ls_config);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given outbound broadcast information, produce a suitable LiquidSoap configuration line for the stream.
|
|
|
|
*
|
|
|
|
* @param Entity\Station $station
|
|
|
|
* @param Entity\StationMountInterface $mount
|
|
|
|
* @param string $idPrefix
|
|
|
|
* @param int $id
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function getOutputString(
|
|
|
|
Entity\Station $station,
|
|
|
|
Entity\StationMountInterface $mount,
|
|
|
|
string $idPrefix,
|
|
|
|
int $id
|
|
|
|
): string {
|
2020-05-02 07:58:59 +00:00
|
|
|
$settings = $station->getBackendConfig();
|
|
|
|
$charset = $settings->getCharset();
|
2020-03-19 19:41:22 +00:00
|
|
|
|
|
|
|
$output_format = $this->getOutputFormatString(
|
|
|
|
$mount->getAutodjFormat(),
|
|
|
|
$mount->getAutodjBitrate() ?? 128
|
|
|
|
);
|
|
|
|
|
|
|
|
$output_params = [];
|
|
|
|
$output_params[] = $output_format;
|
2020-03-22 00:29:07 +00:00
|
|
|
$output_params[] = 'id="' . self::getVarName($station, $idPrefix . $id) . '"';
|
2020-03-19 19:41:22 +00:00
|
|
|
|
|
|
|
$output_params[] = 'host = "' . self::cleanUpString($mount->getAutodjHost()) . '"';
|
|
|
|
$output_params[] = 'port = ' . (int)$mount->getAutodjPort();
|
|
|
|
|
|
|
|
$username = $mount->getAutodjUsername();
|
|
|
|
if (!empty($username)) {
|
|
|
|
$output_params[] = 'user = "' . self::cleanUpString($username) . '"';
|
|
|
|
}
|
|
|
|
|
|
|
|
$password = self::cleanUpString($mount->getAutodjPassword());
|
|
|
|
if ($mount->getAutodjShoutcastMode()) {
|
|
|
|
$password .= ':#' . $id;
|
|
|
|
}
|
|
|
|
$output_params[] = 'password = "' . $password . '"';
|
|
|
|
|
|
|
|
if (!empty($mount->getAutodjMount())) {
|
|
|
|
if ($mount->getAutodjShoutcastMode()) {
|
|
|
|
$output_params[] = 'icy_id = ' . $id;
|
|
|
|
} else {
|
|
|
|
$output_params[] = 'mount = "' . self::cleanUpString($mount->getAutodjMount()) . '"';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$output_params[] = 'name = "' . self::cleanUpString($station->getName()) . '"';
|
|
|
|
$output_params[] = 'description = "' . self::cleanUpString($station->getDescription()) . '"';
|
|
|
|
$output_params[] = 'genre = "' . self::cleanUpString($station->getGenre()) . '"';
|
|
|
|
|
|
|
|
if (!empty($station->getUrl())) {
|
|
|
|
$output_params[] = 'url = "' . self::cleanUpString($station->getUrl()) . '"';
|
|
|
|
}
|
|
|
|
|
|
|
|
$output_params[] = 'public = ' . ($mount->getIsPublic() ? 'true' : 'false');
|
|
|
|
$output_params[] = 'encoding = "' . $charset . '"';
|
|
|
|
|
|
|
|
if ($mount->getAutodjShoutcastMode()) {
|
|
|
|
$output_params[] = 'protocol="icy"';
|
|
|
|
}
|
|
|
|
|
|
|
|
$output_params[] = 'radio';
|
|
|
|
|
|
|
|
return 'output.icecast(' . implode(', ', $output_params) . ')';
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getOutputFormatString(string $format, int $bitrate = 128): string
|
|
|
|
{
|
|
|
|
switch (strtolower($format)) {
|
|
|
|
case Entity\StationMountInterface::FORMAT_AAC:
|
|
|
|
$afterburner = ($bitrate >= 160) ? 'true' : 'false';
|
|
|
|
$aot = ($bitrate >= 96) ? 'mpeg4_aac_lc' : 'mpeg4_he_aac_v2';
|
|
|
|
|
|
|
|
return '%fdkaac(channels=2, samplerate=44100, bitrate=' . $bitrate . ', afterburner=' . $afterburner . ', aot="' . $aot . '", sbr_mode=true)';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Entity\StationMountInterface::FORMAT_OGG:
|
|
|
|
return '%vorbis.cbr(samplerate=44100, channels=2, bitrate=' . $bitrate . ')';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Entity\StationMountInterface::FORMAT_OPUS:
|
2020-06-06 04:50:44 +00:00
|
|
|
return '%opus(samplerate=48000, bitrate=' . $bitrate . ', vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band")';
|
2020-03-19 19:41:22 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case Entity\StationMountInterface::FORMAT_MP3:
|
|
|
|
default:
|
|
|
|
return '%mp3(samplerate=44100, stereo=true, bitrate=' . $bitrate . ', id3v2=true)';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function writeRemoteBroadcastConfiguration(WriteLiquidsoapConfiguration $event): void
|
|
|
|
{
|
|
|
|
$station = $event->getStation();
|
|
|
|
|
|
|
|
$ls_config = [
|
|
|
|
'# Remote Relays',
|
|
|
|
];
|
|
|
|
|
|
|
|
// Set up broadcast to remote relays.
|
|
|
|
$i = 0;
|
|
|
|
foreach ($station->getRemotes() as $remote_row) {
|
|
|
|
$i++;
|
|
|
|
|
|
|
|
/** @var Entity\StationRemote $remote_row */
|
|
|
|
if (!$remote_row->getEnableAutodj()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$ls_config[] = $this->getOutputString($station, $remote_row, 'relay_', $i);
|
|
|
|
}
|
|
|
|
|
|
|
|
$event->appendLines($ls_config);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Filter a user-supplied string to be a valid LiquidSoap config entry.
|
|
|
|
*
|
|
|
|
* @param string $string
|
|
|
|
*
|
|
|
|
* @return mixed
|
|
|
|
*/
|
|
|
|
public static function cleanUpString($string)
|
|
|
|
{
|
|
|
|
return str_replace(['"', "\n", "\r"], ['\'', '', ''], $string);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert an integer or float into a Liquidsoap configuration compatible float.
|
|
|
|
*
|
|
|
|
* @param float|int $number
|
|
|
|
* @param int $decimals
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function toFloat($number, $decimals = 2): string
|
|
|
|
{
|
|
|
|
if ((int)$number == $number) {
|
|
|
|
return (int)$number . '.';
|
|
|
|
}
|
|
|
|
|
|
|
|
return number_format($number, $decimals, '.', '');
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function formatTimeCode($time_code): string
|
|
|
|
{
|
|
|
|
$hours = floor($time_code / 100);
|
|
|
|
$mins = $time_code % 100;
|
|
|
|
|
|
|
|
return $hours . 'h' . $mins . 'm';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given an original name and a station, return a filtered prefixed variable identifying the station.
|
|
|
|
*
|
|
|
|
* @param Entity\Station $station
|
2020-03-22 00:29:07 +00:00
|
|
|
* @param string $original_name
|
2020-03-19 19:41:22 +00:00
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
2020-03-22 00:29:07 +00:00
|
|
|
public static function getVarName(Entity\Station $station, $original_name): string
|
2020-03-19 19:41:22 +00:00
|
|
|
{
|
|
|
|
$short_name = self::cleanUpString($station->getShortName());
|
|
|
|
|
|
|
|
return (!empty($short_name))
|
|
|
|
? $short_name . '_' . $original_name
|
|
|
|
: 'station_' . $station->getId() . '_' . $original_name;
|
|
|
|
}
|
2020-06-06 04:50:44 +00:00
|
|
|
}
|