Split Playlist File writing into its own class; don't write playlist files for most LS changes; lint custom LS config as it's saved.
This commit is contained in:
parent
2c428b12bb
commit
8c4e5251ac
|
@ -194,6 +194,7 @@ return function (CallableEventDispatcherInterface $dispatcher) {
|
|||
App\Radio\AutoDJ\Queue::class,
|
||||
App\Radio\AutoDJ\Annotations::class,
|
||||
App\Radio\Backend\Liquidsoap\ConfigWriter::class,
|
||||
App\Radio\Backend\Liquidsoap\PlaylistFileWriter::class,
|
||||
App\Sync\NowPlaying\Task\NowPlayingTask::class,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -6,15 +6,15 @@ use App\Sync\Task;
|
|||
use Symfony\Component\Mailer;
|
||||
|
||||
return [
|
||||
Message\AddNewMediaMessage::class => Task\CheckMediaTask::class,
|
||||
Message\AddNewMediaMessage::class => Task\CheckMediaTask::class,
|
||||
Message\ReprocessMediaMessage::class => Task\CheckMediaTask::class,
|
||||
|
||||
Message\WritePlaylistFileMessage::class => Liquidsoap\ConfigWriter::class,
|
||||
Message\WritePlaylistFileMessage::class => Liquidsoap\PlaylistFileWriter::class,
|
||||
|
||||
Message\BackupMessage::class => Task\RunBackupTask::class,
|
||||
|
||||
Message\DispatchWebhookMessage::class => App\Webhook\Dispatcher::class,
|
||||
Message\TestWebhookMessage::class => App\Webhook\Dispatcher::class,
|
||||
Message\TestWebhookMessage::class => App\Webhook\Dispatcher::class,
|
||||
|
||||
Mailer\Messenger\SendEmailMessage::class => Mailer\Messenger\MessageHandler::class,
|
||||
];
|
||||
|
|
|
@ -6,9 +6,12 @@ namespace App\Controller\Api\Stations\LiquidsoapConfig;
|
|||
|
||||
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||
use App\Entity;
|
||||
use App\Event\Radio\WriteLiquidsoapConfiguration;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\Backend\Liquidsoap;
|
||||
use App\Radio\Backend\Liquidsoap\ConfigWriter;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class PutAction
|
||||
|
@ -16,7 +19,9 @@ class PutAction
|
|||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
ReloadableEntityManagerInterface $em
|
||||
ReloadableEntityManagerInterface $em,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
Liquidsoap $liquidsoap,
|
||||
): ResponseInterface {
|
||||
$body = (array)$request->getParsedBody();
|
||||
|
||||
|
@ -34,6 +39,16 @@ class PutAction
|
|||
$em->persist($station);
|
||||
$em->flush();
|
||||
|
||||
try {
|
||||
$event = new WriteLiquidsoapConfiguration($station, false, false);
|
||||
$eventDispatcher->dispatch($event);
|
||||
|
||||
$config = $event->buildConfiguration();
|
||||
$liquidsoap->verifyConfig($config);
|
||||
} catch (\Throwable $e) {
|
||||
return $response->withStatus(500)->withJson(Entity\Api\Error::fromException($e));
|
||||
}
|
||||
|
||||
return $response->withJson(Entity\Api\Status::updated());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,13 @@ declare(strict_types=1);
|
|||
namespace App\Controller\Stations;
|
||||
|
||||
use App\Entity\Repository\SettingsRepository;
|
||||
use App\Event\Radio\WriteLiquidsoapConfiguration;
|
||||
use App\Exception\StationUnsupportedException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\Backend\Liquidsoap;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class EditLiquidsoapConfigAction
|
||||
|
@ -18,7 +20,8 @@ class EditLiquidsoapConfigAction
|
|||
ServerRequest $request,
|
||||
Response $response,
|
||||
EntityManagerInterface $em,
|
||||
SettingsRepository $settingsRepo
|
||||
SettingsRepository $settingsRepo,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
|
@ -28,20 +31,20 @@ class EditLiquidsoapConfigAction
|
|||
}
|
||||
|
||||
$configSections = Liquidsoap\ConfigWriter::getCustomConfigurationSections();
|
||||
$config = $backend->getEditableConfiguration($station);
|
||||
$tokens = Liquidsoap\ConfigWriter::getDividerString();
|
||||
|
||||
$event = new WriteLiquidsoapConfiguration($station, true, false);
|
||||
$eventDispatcher->dispatch($event);
|
||||
$config = $event->buildConfiguration();
|
||||
|
||||
$areas = [];
|
||||
|
||||
$tok = strtok($config, $tokens);
|
||||
$i = 0;
|
||||
while ($tok !== false) {
|
||||
$tok = trim($tok);
|
||||
$i++;
|
||||
|
||||
if (in_array($tok, $configSections, true)) {
|
||||
$areas[] = [
|
||||
'is_field' => true,
|
||||
'is_field' => true,
|
||||
'field_name' => $tok,
|
||||
];
|
||||
} else {
|
||||
|
|
|
@ -13,7 +13,8 @@ class WriteLiquidsoapConfiguration extends Event
|
|||
|
||||
public function __construct(
|
||||
protected Station $station,
|
||||
protected bool $forEditing = false
|
||||
protected bool $forEditing = false,
|
||||
protected bool $writeToDisk = true
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -27,6 +28,11 @@ class WriteLiquidsoapConfiguration extends Event
|
|||
return $this->forEditing;
|
||||
}
|
||||
|
||||
public function shouldWriteToDisk(): bool
|
||||
{
|
||||
return $this->writeToDisk && !$this->forEditing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one of more lines to the end of the configuration string.
|
||||
*
|
||||
|
|
|
@ -45,17 +45,7 @@ class Liquidsoap extends AbstractBackend
|
|||
*/
|
||||
public function getCurrentConfiguration(Entity\Station $station): ?string
|
||||
{
|
||||
return $this->doGetConfiguration($station);
|
||||
}
|
||||
|
||||
public function getEditableConfiguration(Entity\Station $station): string
|
||||
{
|
||||
return $this->doGetConfiguration($station, true);
|
||||
}
|
||||
|
||||
protected function doGetConfiguration(Entity\Station $station, bool $forEditing = false): string
|
||||
{
|
||||
$event = new WriteLiquidsoapConfiguration($station, $forEditing);
|
||||
$event = new WriteLiquidsoapConfiguration($station, false, true);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $event->buildConfiguration();
|
||||
|
@ -328,4 +318,22 @@ class Liquidsoap extends AbstractBackend
|
|||
->withScheme('wss')
|
||||
->withPath($base_url->getPath() . '/radio/' . $stream_port . $djMount);
|
||||
}
|
||||
|
||||
public function verifyConfig(string $config): void
|
||||
{
|
||||
$binary = $this->getBinary();
|
||||
|
||||
$process = new Process([
|
||||
$binary,
|
||||
'--check',
|
||||
'-',
|
||||
]);
|
||||
|
||||
$process->setInput($config);
|
||||
$process->run();
|
||||
|
||||
if (1 === $process->getExitCode()) {
|
||||
throw new \LogicException($process->getOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,7 @@ namespace App\Radio\Backend\Liquidsoap;
|
|||
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Event\Radio\AnnotateNextSong;
|
||||
use App\Event\Radio\WriteLiquidsoapConfiguration;
|
||||
use App\Exception;
|
||||
use App\Flysystem\StationFilesystems;
|
||||
use App\Message;
|
||||
use App\Radio\Backend\Liquidsoap;
|
||||
use App\Radio\Enums\FrontendAdapters;
|
||||
use App\Radio\Enums\StreamFormats;
|
||||
|
@ -18,7 +14,6 @@ use App\Radio\Enums\StreamProtocols;
|
|||
use App\Radio\FallbackFile;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use League\Flysystem\StorageAttributes;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
@ -43,22 +38,6 @@ class ConfigWriter implements EventSubscriberInterface
|
|||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle event dispatch.
|
||||
*
|
||||
* @param Message\AbstractMessage $message
|
||||
*/
|
||||
public function __invoke(Message\AbstractMessage $message): void
|
||||
{
|
||||
if ($message instanceof Message\WritePlaylistFileMessage) {
|
||||
$playlist = $this->em->find(Entity\StationPlaylist::class, $message->playlist_id);
|
||||
|
||||
if ($playlist instanceof Entity\StationPlaylist) {
|
||||
$this->writePlaylistFile($playlist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
|
@ -213,7 +192,7 @@ class ConfigWriter implements EventSubscriberInterface
|
|||
$event->appendBlock(
|
||||
<<<EOF
|
||||
station_media_dir = "${stationMediaDir}"
|
||||
def azuracast_media_protocol(~rlog,~maxtime,arg) =
|
||||
def azuracast_media_protocol(~rlog=_,~maxtime=_,arg) =
|
||||
["#{station_media_dir}/#{arg}"]
|
||||
end
|
||||
EOF
|
||||
|
@ -221,11 +200,13 @@ class ConfigWriter implements EventSubscriberInterface
|
|||
} else {
|
||||
$event->appendBlock(
|
||||
<<<EOF
|
||||
def azuracast_media_protocol(~rlog,~maxtime,arg) =
|
||||
def azuracast_media_protocol(~rlog=_,~maxtime,arg) =
|
||||
timeout = int_of_float(maxtime - time())
|
||||
|
||||
j = json()
|
||||
j.add("uri", arg)
|
||||
|
||||
[azuracast_api_call(timeout=20, "cp", json.stringify(j))]
|
||||
[azuracast_api_call(timeout=timeout, "cp", json.stringify(j))]
|
||||
end
|
||||
EOF
|
||||
);
|
||||
|
@ -271,37 +252,13 @@ class ConfigWriter implements EventSubscriberInterface
|
|||
|
||||
$this->writeCustomConfigurationSection($event, self::CUSTOM_PRE_PLAYLISTS);
|
||||
|
||||
// Clear out existing playlists directory.
|
||||
|
||||
$fsPlaylists = (new StationFilesystems($station))->getPlaylistsFilesystem();
|
||||
|
||||
foreach ($fsPlaylists->listContents('', false) as $file) {
|
||||
/** @var StorageAttributes $file */
|
||||
if ($file->isDir()) {
|
||||
$fsPlaylists->deleteDirectory($file->path());
|
||||
} else {
|
||||
$fsPlaylists->delete($file->path());
|
||||
}
|
||||
}
|
||||
|
||||
// Set up playlists using older format as a fallback.
|
||||
$playlistObjects = [];
|
||||
|
||||
foreach ($station->getPlaylists() as $playlistRaw) {
|
||||
/** @var Entity\StationPlaylist $playlistRaw */
|
||||
if (!$playlistRaw->getIsEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$playlistObjects[] = $playlistRaw;
|
||||
}
|
||||
|
||||
$playlistVarNames = [];
|
||||
$genPlaylistWeights = [];
|
||||
$genPlaylistVars = [];
|
||||
|
||||
$specialPlaylists = [
|
||||
'once_per_x_songs' => [
|
||||
'once_per_x_songs' => [
|
||||
'# Once per x Songs Playlists',
|
||||
],
|
||||
'once_per_x_minutes' => [
|
||||
|
@ -312,9 +269,12 @@ class ConfigWriter implements EventSubscriberInterface
|
|||
$scheduleSwitches = [];
|
||||
$scheduleSwitchesInterrupting = [];
|
||||
|
||||
foreach ($playlistObjects as $playlist) {
|
||||
/** @var Entity\StationPlaylist $playlist */
|
||||
$playlistVarName = self::cleanUpVarName('playlist_' . $playlist->getShortName());
|
||||
foreach ($station->getPlaylists() as $playlist) {
|
||||
if (!$playlist->getIsEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$playlistVarName = self::getPlaylistVariableName($playlist);
|
||||
|
||||
if (in_array($playlistVarName, $playlistVarNames, true)) {
|
||||
$playlistVarName .= '_' . $playlist->getId();
|
||||
|
@ -325,10 +285,7 @@ class ConfigWriter implements EventSubscriberInterface
|
|||
$playlistConfigLines = [];
|
||||
|
||||
if (Entity\Enums\PlaylistSources::Songs === $playlist->getSourceEnum()) {
|
||||
$playlistFilePath = $this->writePlaylistFile($playlist, false);
|
||||
if (!$playlistFilePath) {
|
||||
continue;
|
||||
}
|
||||
$playlistFilePath = PlaylistFileWriter::getPlaylistFilePath($playlist);
|
||||
|
||||
$playlistParams = [
|
||||
'id="' . self::cleanUpString($playlistVarName) . '"',
|
||||
|
@ -596,79 +553,6 @@ class ConfigWriter implements EventSubscriberInterface
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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|null The full path that was written to.
|
||||
*/
|
||||
public function writePlaylistFile(Entity\StationPlaylist $playlist, bool $notify = true): ?string
|
||||
{
|
||||
$station = $playlist->getStation();
|
||||
|
||||
$playlistPath = $station->getRadioPlaylistsDir();
|
||||
$playlistVarName = 'playlist_' . $playlist->getShortName();
|
||||
|
||||
$this->logger->info(
|
||||
'Writing playlist file to disk...',
|
||||
[
|
||||
'station' => $station->getName(),
|
||||
'playlist' => $playlist->getName(),
|
||||
]
|
||||
);
|
||||
|
||||
$playlistFile = [];
|
||||
|
||||
$mediaQuery = $this->em->createQuery(
|
||||
<<<'DQL'
|
||||
SELECT DISTINCT sm
|
||||
FROM App\Entity\StationMedia sm
|
||||
JOIN sm.playlists spm
|
||||
WHERE spm.playlist = :playlist
|
||||
ORDER BY spm.weight ASC
|
||||
DQL
|
||||
)->setParameter('playlist', $playlist);
|
||||
|
||||
/** @var Entity\StationMedia $mediaFile */
|
||||
foreach ($mediaQuery->toIterable() as $mediaFile) {
|
||||
$event = new AnnotateNextSong(
|
||||
station: $station,
|
||||
media: $mediaFile,
|
||||
playlist: $playlist,
|
||||
asAutoDj: false
|
||||
);
|
||||
|
||||
try {
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
$playlistFile[] = $event->buildAnnotations();
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$playlistFilePath = $playlistPath . '/' . $playlistVarName . '.m3u';
|
||||
|
||||
file_put_contents($playlistFilePath, implode("\n", $playlistFile));
|
||||
|
||||
if ($notify) {
|
||||
try {
|
||||
$this->liquidsoap->command($station, $playlistVarName . '.reload');
|
||||
} catch (Exception $e) {
|
||||
$this->logger->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.
|
||||
|
@ -1278,4 +1162,9 @@ class ConfigWriter implements EventSubscriberInterface
|
|||
|
||||
return $str;
|
||||
}
|
||||
|
||||
public static function getPlaylistVariableName(Entity\StationPlaylist $playlist): string
|
||||
{
|
||||
return 'playlist_' . $playlist->getShortName();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Radio\Backend\Liquidsoap;
|
||||
|
||||
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||
use App\Entity;
|
||||
use App\Event\Radio\AnnotateNextSong;
|
||||
use App\Event\Radio\WriteLiquidsoapConfiguration;
|
||||
use App\Exception;
|
||||
use App\Flysystem\StationFilesystems;
|
||||
use App\Message;
|
||||
use App\Radio\Backend\Liquidsoap;
|
||||
use League\Flysystem\StorageAttributes;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class PlaylistFileWriter implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected LoggerInterface $logger,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected ReloadableEntityManagerInterface $em,
|
||||
protected Filesystem $fsUtils,
|
||||
protected Liquidsoap $liquidsoap,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle event dispatch.
|
||||
*
|
||||
* @param Message\AbstractMessage $message
|
||||
*/
|
||||
public function __invoke(Message\AbstractMessage $message): void
|
||||
{
|
||||
if ($message instanceof Message\WritePlaylistFileMessage) {
|
||||
$playlist = $this->em->find(Entity\StationPlaylist::class, $message->playlist_id);
|
||||
|
||||
if ($playlist instanceof Entity\StationPlaylist) {
|
||||
$this->writePlaylistFile($playlist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
WriteLiquidsoapConfiguration::class => [
|
||||
['writePlaylistsForLiquidsoap', 32],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function writePlaylistsForLiquidsoap(WriteLiquidsoapConfiguration $event): void
|
||||
{
|
||||
if ($event->shouldWriteToDisk()) {
|
||||
$this->writeAllPlaylistFiles($event->getStation());
|
||||
}
|
||||
}
|
||||
|
||||
public function writeAllPlaylistFiles(Entity\Station $station): void
|
||||
{
|
||||
// Clear out existing playlists directory.
|
||||
$fsPlaylists = (new StationFilesystems($station))->getPlaylistsFilesystem();
|
||||
|
||||
foreach ($fsPlaylists->listContents('', false) as $file) {
|
||||
/** @var StorageAttributes $file */
|
||||
if ($file->isDir()) {
|
||||
$fsPlaylists->deleteDirectory($file->path());
|
||||
} else {
|
||||
$fsPlaylists->delete($file->path());
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($station->getPlaylists() as $playlist) {
|
||||
if (!$playlist->getIsEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->writePlaylistFile($playlist, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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|null The full path that was written to.
|
||||
*/
|
||||
public function writePlaylistFile(Entity\StationPlaylist $playlist, bool $notify = true): ?string
|
||||
{
|
||||
$station = $playlist->getStation();
|
||||
|
||||
$this->logger->info(
|
||||
'Writing playlist file to disk...',
|
||||
[
|
||||
'station' => $station->getName(),
|
||||
'playlist' => $playlist->getName(),
|
||||
]
|
||||
);
|
||||
|
||||
$playlistFile = [];
|
||||
|
||||
$mediaQuery = $this->em->createQuery(
|
||||
<<<'DQL'
|
||||
SELECT DISTINCT sm
|
||||
FROM App\Entity\StationMedia sm
|
||||
JOIN sm.playlists spm
|
||||
WHERE spm.playlist = :playlist
|
||||
ORDER BY spm.weight ASC
|
||||
DQL
|
||||
)->setParameter('playlist', $playlist);
|
||||
|
||||
/** @var Entity\StationMedia $mediaFile */
|
||||
foreach ($mediaQuery->toIterable() as $mediaFile) {
|
||||
$event = new AnnotateNextSong(
|
||||
station: $station,
|
||||
media: $mediaFile,
|
||||
playlist: $playlist,
|
||||
asAutoDj: false
|
||||
);
|
||||
|
||||
try {
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
$playlistFile[] = $event->buildAnnotations();
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$playlistVarName = ConfigWriter::getPlaylistVariableName($playlist);
|
||||
$playlistFilePath = self::getPlaylistFilePath($playlist);
|
||||
|
||||
$this->fsUtils->dumpFile(
|
||||
$playlistFilePath,
|
||||
implode("\n", $playlistFile)
|
||||
);
|
||||
|
||||
if ($notify) {
|
||||
try {
|
||||
$this->liquidsoap->command($station, $playlistVarName . '.reload');
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error(
|
||||
'Could not reload playlist with AutoDJ.',
|
||||
[
|
||||
'message' => $e->getMessage(),
|
||||
'playlist' => $playlistVarName,
|
||||
'station' => $station->getId(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $playlistFilePath;
|
||||
}
|
||||
|
||||
public static function getPlaylistFilePath(Entity\StationPlaylist $playlist): string
|
||||
{
|
||||
return $playlist->getStation()->getRadioPlaylistsDir() . '/'
|
||||
. ConfigWriter::getPlaylistVariableName($playlist) . '.m3u';
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue