Refactor adapters; move some static functions to be non-static and implement a better change tracking system.
This commit is contained in:
parent
686f480d7c
commit
4ccddeb5f3
|
@ -6,13 +6,17 @@ use App\Entity\StationFrontendConfiguration;
|
|||
use App\Entity\StationMountInterface;
|
||||
use App\Radio\Adapters;
|
||||
|
||||
$frontends = Adapters::listFrontendAdapters(true);
|
||||
/**
|
||||
* @var Adapters $adapters
|
||||
*/
|
||||
|
||||
$frontends = $adapters->listFrontendAdapters(true);
|
||||
$frontend_types = [];
|
||||
foreach ($frontends as $adapter_nickname => $adapter_info) {
|
||||
$frontend_types[$adapter_nickname] = $adapter_info['name'];
|
||||
}
|
||||
|
||||
$backends = Adapters::listBackendAdapters(true);
|
||||
$backends = $adapters->listBackendAdapters(true);
|
||||
$backend_types = [];
|
||||
foreach ($backends as $adapter_nickname => $adapter_info) {
|
||||
$backend_types[$adapter_nickname] = $adapter_info['name'];
|
||||
|
|
|
@ -60,21 +60,21 @@ return function (App\Event\BuildStationMenu $e) {
|
|||
'label' => __('Music Files'),
|
||||
'icon' => 'library_music',
|
||||
'url' => $router->fromHere('stations:files:index'),
|
||||
'visible' => $backend::supportsMedia(),
|
||||
'visible' => $backend->supportsMedia(),
|
||||
'permission' => Acl::STATION_MEDIA,
|
||||
],
|
||||
'playlists' => [
|
||||
'label' => __('Playlists'),
|
||||
'icon' => 'queue_music',
|
||||
'url' => $router->fromHere('stations:playlists:index'),
|
||||
'visible' => $backend::supportsMedia(),
|
||||
'visible' => $backend->supportsMedia(),
|
||||
'permission' => Acl::STATION_MEDIA,
|
||||
],
|
||||
'streamers' => [
|
||||
'label' => __('Streamer/DJ Accounts'),
|
||||
'icon' => 'mic',
|
||||
'url' => $router->fromHere('stations:streamers:index'),
|
||||
'visible' => $backend::supportsStreamers(),
|
||||
'visible' => $backend->supportsStreamers(),
|
||||
'permission' => Acl::STATION_STREAMERS,
|
||||
],
|
||||
'web_dj' => [
|
||||
|
@ -89,7 +89,7 @@ return function (App\Event\BuildStationMenu $e) {
|
|||
'label' => __('Mount Points'),
|
||||
'icon' => 'wifi_tethering',
|
||||
'url' => $router->fromHere('stations:mounts:index'),
|
||||
'visible' => $frontend::supportsMounts(),
|
||||
'visible' => $frontend->supportsMounts(),
|
||||
'permission' => Acl::STATION_MOUNTS,
|
||||
],
|
||||
'remotes' => [
|
||||
|
@ -116,7 +116,7 @@ return function (App\Event\BuildStationMenu $e) {
|
|||
'reports_listeners' => [
|
||||
'label' => __('Listeners'),
|
||||
'url' => $router->fromHere('stations:reports:listeners'),
|
||||
'visible' => $frontend::supportsListenerDetail(),
|
||||
'visible' => $frontend->supportsListenerDetail(),
|
||||
],
|
||||
'reports_requests' => [
|
||||
'label' => __('Song Requests'),
|
||||
|
@ -130,22 +130,22 @@ return function (App\Event\BuildStationMenu $e) {
|
|||
'reports_performance' => [
|
||||
'label' => __('Song Listener Impact'),
|
||||
'url' => $router->fromHere('stations:reports:performance'),
|
||||
'visible' => $backend::supportsMedia(),
|
||||
'visible' => $backend->supportsMedia(),
|
||||
],
|
||||
'reports_duplicates' => [
|
||||
'label' => __('Duplicate Songs'),
|
||||
'url' => $router->fromHere('stations:files:index') . '#special:duplicates',
|
||||
'visible' => $backend::supportsMedia(),
|
||||
'visible' => $backend->supportsMedia(),
|
||||
],
|
||||
'reports_unprocessable' => [
|
||||
'label' => __('Unprocessable Files'),
|
||||
'url' => $router->fromHere('stations:files:index') . '#special:unprocessable',
|
||||
'visible' => $backend::supportsMedia(),
|
||||
'visible' => $backend->supportsMedia(),
|
||||
],
|
||||
'reports_soundexchange' => [
|
||||
'label' => __('SoundExchange Royalties'),
|
||||
'url' => $router->fromHere('stations:reports:soundexchange'),
|
||||
'visible' => $frontend::supportsListenerDetail(),
|
||||
'visible' => $frontend->supportsListenerDetail(),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
@ -162,7 +162,7 @@ return function (App\Event\BuildStationMenu $e) {
|
|||
'automation' => [
|
||||
'label' => __('Automated Assignment'),
|
||||
'url' => $router->fromHere('stations:automation:index'),
|
||||
'visible' => $backend::supportsMedia(),
|
||||
'visible' => $backend->supportsMedia(),
|
||||
'permission' => Acl::STATION_AUTOMATION,
|
||||
],
|
||||
'ls_config' => [
|
||||
|
|
|
@ -36,17 +36,10 @@ class RestartRadioCommand extends CommandAbstract
|
|||
$io->progressStart(count($stations));
|
||||
|
||||
foreach ($stations as $station) {
|
||||
$configuration->writeConfiguration($station, false, true);
|
||||
|
||||
$station->setHasStarted(true);
|
||||
$station->setNeedsRestart(false);
|
||||
$em->persist($station);
|
||||
|
||||
$configuration->writeConfiguration($station, true);
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
$io->progressFinish();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -19,9 +19,14 @@ class InstallShoutcastController
|
|||
{
|
||||
protected array $form_config;
|
||||
|
||||
public function __construct(Config $config)
|
||||
{
|
||||
protected SHOUTcast $adapter;
|
||||
|
||||
public function __construct(
|
||||
Config $config,
|
||||
SHOUTcast $adapter
|
||||
) {
|
||||
$this->form_config = $config->get('forms/install_shoutcast');
|
||||
$this->adapter = $adapter;
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
|
@ -31,7 +36,7 @@ class InstallShoutcastController
|
|||
): ResponseInterface {
|
||||
$form_config = $this->form_config;
|
||||
|
||||
$version = SHOUTcast::getVersion();
|
||||
$version = $this->adapter->getVersion();
|
||||
|
||||
if (null !== $version) {
|
||||
$form_config['groups'][0]['elements']['current_version'][1]['markup'] = '<p class="text-success">' . __(
|
||||
|
@ -58,11 +63,14 @@ class InstallShoutcastController
|
|||
|
||||
$import_file->moveTo($sc_tgz_path);
|
||||
|
||||
$process = new Process([
|
||||
'tar',
|
||||
'xvzf',
|
||||
$sc_tgz_path,
|
||||
], $sc_base_dir);
|
||||
$process = new Process(
|
||||
[
|
||||
'tar',
|
||||
'xvzf',
|
||||
$sc_tgz_path,
|
||||
],
|
||||
$sc_base_dir
|
||||
);
|
||||
|
||||
$process->mustRun();
|
||||
|
||||
|
@ -77,10 +85,14 @@ class InstallShoutcastController
|
|||
}
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'system/form_page', [
|
||||
'form' => $form,
|
||||
'render_mode' => 'edit',
|
||||
'title' => __('Install SHOUTcast'),
|
||||
]);
|
||||
return $request->getView()->renderToResponse(
|
||||
$response,
|
||||
'system/form_page',
|
||||
[
|
||||
'form' => $form,
|
||||
'render_mode' => 'edit',
|
||||
'title' => __('Install SHOUTcast'),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ class MountsController extends AbstractStationApiCrudController
|
|||
$station = parent::getStation($request);
|
||||
|
||||
$frontend = $request->getStationFrontend();
|
||||
if (!$frontend::supportsMounts()) {
|
||||
if (!$frontend->supportsMounts()) {
|
||||
throw new StationUnsupportedException();
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ class RequestsController
|
|||
|
||||
// Verify that the station supports requests.
|
||||
$ba = $request->getStationBackend();
|
||||
if (!$ba::supportsRequests() || !$station->getEnableRequests()) {
|
||||
if (!$ba->supportsRequests() || !$station->getEnableRequests()) {
|
||||
return $response->withStatus(403)
|
||||
->withJson(new Entity\Api\Error(403, __('This station does not accept requests currently.')));
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ class RequestsController
|
|||
|
||||
// Verify that the station supports requests.
|
||||
$ba = $request->getStationBackend();
|
||||
if (!$ba::supportsRequests() || !$station->getEnableRequests()) {
|
||||
if (!$ba->supportsRequests() || !$station->getEnableRequests()) {
|
||||
return $response->withStatus(403)
|
||||
->withJson(new Entity\Api\Error(403, __('This station does not accept requests currently.')));
|
||||
}
|
||||
|
|
|
@ -49,10 +49,12 @@ class ServicesController
|
|||
$backend = $request->getStationBackend();
|
||||
$frontend = $request->getStationFrontend();
|
||||
|
||||
return $response->withJson(new Entity\Api\StationServiceStatus(
|
||||
$backend->isRunning($station),
|
||||
$frontend->isRunning($station)
|
||||
));
|
||||
return $response->withJson(
|
||||
new Entity\Api\StationServiceStatus(
|
||||
$backend->isRunning($station),
|
||||
$frontend->isRunning($station)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,7 +73,7 @@ class ServicesController
|
|||
public function restartAction(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
$station = $request->getStation();
|
||||
$this->configuration->writeConfiguration($station, false, true);
|
||||
$this->configuration->writeConfiguration($station, true);
|
||||
|
||||
return $response->withJson(new Entity\Api\Status(true, __('Station restarted.')));
|
||||
}
|
||||
|
|
|
@ -179,7 +179,7 @@ class StreamersController extends AbstractScheduledEntityController
|
|||
$station = parent::getStation($request);
|
||||
|
||||
$backend = $request->getStationBackend();
|
||||
if (!$backend::supportsStreamers()) {
|
||||
if (!$backend->supportsStreamers()) {
|
||||
throw new StationUnsupportedException();
|
||||
}
|
||||
|
||||
|
|
|
@ -23,15 +23,19 @@ class MountsController extends AbstractStationCrudController
|
|||
$station = $request->getStation();
|
||||
$frontend = $request->getStationFrontend();
|
||||
|
||||
if (!$frontend::supportsMounts()) {
|
||||
if (!$frontend->supportsMounts()) {
|
||||
throw new StationUnsupportedException(__('This feature is not currently supported on this station.'));
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/mounts/index', [
|
||||
'frontend_type' => $station->getFrontendType(),
|
||||
'mounts' => $station->getMounts(),
|
||||
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
|
||||
]);
|
||||
return $request->getView()->renderToResponse(
|
||||
$response,
|
||||
'stations/mounts/index',
|
||||
[
|
||||
'frontend_type' => $station->getFrontendType(),
|
||||
'mounts' => $station->getMounts(),
|
||||
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function editAction(ServerRequest $request, Response $response, $id = null): ResponseInterface
|
||||
|
@ -41,11 +45,15 @@ class MountsController extends AbstractStationCrudController
|
|||
return $response->withRedirect($request->getRouter()->fromHere('stations:mounts:index'));
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/mounts/edit', [
|
||||
'form' => $this->form,
|
||||
'render_mode' => 'edit',
|
||||
'title' => $id ? __('Edit Mount Point') : __('Add Mount Point'),
|
||||
]);
|
||||
return $request->getView()->renderToResponse(
|
||||
$response,
|
||||
'stations/mounts/edit',
|
||||
[
|
||||
'form' => $this->form,
|
||||
'render_mode' => 'edit',
|
||||
'title' => $id ? __('Edit Mount Point') : __('Add Mount Point'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteAction(
|
||||
|
|
|
@ -14,12 +14,16 @@ class PlaylistsController
|
|||
$station = $request->getStation();
|
||||
|
||||
$backend = $request->getStationBackend();
|
||||
if (!$backend::supportsMedia()) {
|
||||
if (!$backend->supportsMedia()) {
|
||||
throw new Exception(__('This feature is not currently supported on this station.'));
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/playlists/index', [
|
||||
'station_tz' => $station->getTimezone(),
|
||||
]);
|
||||
return $request->getView()->renderToResponse(
|
||||
$response,
|
||||
'stations/playlists/index',
|
||||
[
|
||||
'station_tz' => $station->getTimezone(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class StreamersController
|
|||
$station = $request->getStation();
|
||||
$backend = $request->getStationBackend();
|
||||
|
||||
if (!$backend::supportsStreamers()) {
|
||||
if (!$backend->supportsStreamers()) {
|
||||
throw new StationUnsupportedException();
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class StationApiGenerator
|
|||
$response->listen_url = $fa->getStreamUrl($station, $baseUri);
|
||||
|
||||
$mounts = [];
|
||||
if ($fa::supportsMounts() && $station->getMounts()->count() > 0) {
|
||||
if ($fa->supportsMounts() && $station->getMounts()->count() > 0) {
|
||||
foreach ($station->getMounts() as $mount) {
|
||||
if ($showAllMounts || $mount->isVisibleOnPublicPages()) {
|
||||
$mounts[] = $mount->api($fa, $baseUri);
|
||||
|
|
|
@ -173,9 +173,9 @@ class StationRepository extends Repository
|
|||
}
|
||||
|
||||
// Create default mountpoints if station supports them.
|
||||
if ($frontend_adapter::supportsMounts()) {
|
||||
if ($frontend_adapter->supportsMounts()) {
|
||||
// Create default mount points.
|
||||
$mount_points = $frontend_adapter::getDefaultMounts();
|
||||
$mount_points = $frontend_adapter->getDefaultMounts();
|
||||
|
||||
foreach ($mount_points as $mount_point) {
|
||||
$mount_record = new Entity\StationMount($station);
|
||||
|
@ -231,15 +231,8 @@ class StationRepository extends Repository
|
|||
*/
|
||||
public function create(Entity\Station $station): Entity\Station
|
||||
{
|
||||
// Create path for station.
|
||||
$station->ensureDirectoriesExist();
|
||||
|
||||
$this->em->persist($station);
|
||||
$this->em->persist($station->getMediaStorageLocation());
|
||||
$this->em->persist($station->getRecordingsStorageLocation());
|
||||
|
||||
// Generate station ID.
|
||||
$this->em->flush();
|
||||
$station->generateAdapterApiKey();
|
||||
$this->configuration->initializeConfiguration($station);
|
||||
|
||||
// Scan directory for any existing files.
|
||||
set_time_limit(600);
|
||||
|
@ -253,22 +246,10 @@ class StationRepository extends Repository
|
|||
/** @var Entity\Station $station */
|
||||
$station = $this->em->find(Entity\Station::class, $station->getId());
|
||||
|
||||
// Load adapters.
|
||||
$frontend_adapter = $this->adapters->getFrontendAdapter($station);
|
||||
|
||||
// Create default mountpoints if station supports them.
|
||||
$frontend_adapter = $this->adapters->getFrontendAdapter($station);
|
||||
$this->resetMounts($station, $frontend_adapter);
|
||||
|
||||
// Load configuration from adapter to pull source and admin PWs.
|
||||
$frontend_adapter->read($station);
|
||||
|
||||
// Write the adapter configurations and update supervisord.
|
||||
$this->configuration->writeConfiguration($station, true);
|
||||
|
||||
// Save changes and continue to the last setup step.
|
||||
$this->em->persist($station);
|
||||
$this->em->flush();
|
||||
|
||||
return $station;
|
||||
}
|
||||
|
||||
|
|
|
@ -488,31 +488,7 @@ class Station
|
|||
$frontend_config = $config;
|
||||
}
|
||||
|
||||
$config = $frontend_config->toArray();
|
||||
|
||||
if ($this->frontend_config != $config) {
|
||||
$this->setNeedsRestart(true);
|
||||
}
|
||||
|
||||
$this->frontend_config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set frontend configuration but do not overwrite existing values.
|
||||
*
|
||||
* @param array $default_config
|
||||
*/
|
||||
public function setFrontendConfigDefaults(array $default_config): void
|
||||
{
|
||||
$frontend_config = (array)$this->frontend_config;
|
||||
|
||||
foreach ($default_config as $config_key => $config_value) {
|
||||
if (empty($frontend_config[$config_key])) {
|
||||
$frontend_config[$config_key] = $config_value;
|
||||
}
|
||||
}
|
||||
|
||||
$this->frontend_config = $frontend_config;
|
||||
$this->frontend_config = $frontend_config->toArray();
|
||||
}
|
||||
|
||||
public function getBackendType(): ?string
|
||||
|
@ -572,13 +548,7 @@ class Station
|
|||
$backend_config = $config;
|
||||
}
|
||||
|
||||
$config = $backend_config->toArray();
|
||||
|
||||
if ($this->backend_config != $config) {
|
||||
$this->setNeedsRestart(true);
|
||||
}
|
||||
|
||||
$this->backend_config = $config;
|
||||
$this->backend_config = $backend_config->toArray();
|
||||
}
|
||||
|
||||
public function getAdapterApiKey(): ?string
|
||||
|
|
|
@ -2,10 +2,22 @@
|
|||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Utilities\Strings;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
class StationFrontendConfiguration extends ArrayCollection
|
||||
{
|
||||
public function __construct(array $elements = [])
|
||||
{
|
||||
// Generate defaults if not set.
|
||||
$elements[self::SOURCE_PASSWORD] ??= Strings::generatePassword();
|
||||
$elements[self::ADMIN_PASSWORD] ??= Strings::generatePassword();
|
||||
$elements[self::RELAY_PASSWORD] ??= Strings::generatePassword();
|
||||
$elements[self::STREAMER_PASSWORD] ??= Strings::generatePassword();
|
||||
|
||||
parent::__construct($elements);
|
||||
}
|
||||
|
||||
public const CUSTOM_CONFIGURATION = 'custom_config';
|
||||
|
||||
public function getCustomConfiguration(): ?string
|
||||
|
@ -20,48 +32,48 @@ class StationFrontendConfiguration extends ArrayCollection
|
|||
|
||||
public const SOURCE_PASSWORD = 'source_pw';
|
||||
|
||||
public function getSourcePassword(): ?string
|
||||
public function getSourcePassword(): string
|
||||
{
|
||||
return $this->get(self::SOURCE_PASSWORD);
|
||||
}
|
||||
|
||||
public function setSourcePassword(?string $pw): void
|
||||
public function setSourcePassword(string $pw): void
|
||||
{
|
||||
$this->set(self::SOURCE_PASSWORD, $pw);
|
||||
}
|
||||
|
||||
public const ADMIN_PASSWORD = 'admin_pw';
|
||||
|
||||
public function getAdminPassword(): ?string
|
||||
public function getAdminPassword(): string
|
||||
{
|
||||
return $this->get(self::ADMIN_PASSWORD);
|
||||
}
|
||||
|
||||
public function setAdminPassword(?string $pw): void
|
||||
public function setAdminPassword(string $pw): void
|
||||
{
|
||||
$this->set(self::ADMIN_PASSWORD, $pw);
|
||||
}
|
||||
|
||||
public const RELAY_PASSWORD = 'relay_pw';
|
||||
|
||||
public function getRelayPassword(): ?string
|
||||
public function getRelayPassword(): string
|
||||
{
|
||||
return $this->get(self::RELAY_PASSWORD);
|
||||
}
|
||||
|
||||
public function setRelayPassword(?string $pw): void
|
||||
public function setRelayPassword(string $pw): void
|
||||
{
|
||||
$this->set(self::RELAY_PASSWORD, $pw);
|
||||
}
|
||||
|
||||
public const STREAMER_PASSWORD = 'streamer_pw';
|
||||
|
||||
public function getStreamerPassword(): ?string
|
||||
public function getStreamerPassword(): string
|
||||
{
|
||||
return $this->get(self::STREAMER_PASSWORD);
|
||||
}
|
||||
|
||||
public function setStreamerPassword(?string $pw): void
|
||||
public function setStreamerPassword(string $pw): void
|
||||
{
|
||||
$this->set(self::STREAMER_PASSWORD, $pw);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Entity;
|
|||
use App\Environment;
|
||||
use App\Flysystem\FilesystemManager;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\Adapters;
|
||||
use App\Radio\Configuration;
|
||||
use App\Sync\Task\CheckMediaTask;
|
||||
use DeepCopy;
|
||||
|
@ -30,10 +31,11 @@ class StationCloneForm extends StationForm
|
|||
ValidatorInterface $validator,
|
||||
Entity\Repository\StationRepository $station_repo,
|
||||
Entity\Repository\StorageLocationRepository $storageLocationRepo,
|
||||
Configuration $configuration,
|
||||
CheckMediaTask $media_sync,
|
||||
Config $config,
|
||||
Environment $environment,
|
||||
Adapters $adapters,
|
||||
Configuration $configuration,
|
||||
CheckMediaTask $media_sync,
|
||||
FilesystemManager $filesystem
|
||||
) {
|
||||
parent::__construct(
|
||||
|
@ -43,7 +45,8 @@ class StationCloneForm extends StationForm
|
|||
$station_repo,
|
||||
$storageLocationRepo,
|
||||
$config,
|
||||
$environment
|
||||
$environment,
|
||||
$adapters
|
||||
);
|
||||
|
||||
$form_config = $config->get('forms/station_clone');
|
||||
|
|
|
@ -7,7 +7,7 @@ use App\Config;
|
|||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\Frontend\SHOUTcast;
|
||||
use App\Radio\Adapters;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\ConstraintViolation;
|
||||
|
@ -21,6 +21,8 @@ class StationForm extends EntityForm
|
|||
|
||||
protected Environment $environment;
|
||||
|
||||
protected Adapters $adapters;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
Serializer $serializer,
|
||||
|
@ -28,14 +30,21 @@ class StationForm extends EntityForm
|
|||
Entity\Repository\StationRepository $station_repo,
|
||||
Entity\Repository\StorageLocationRepository $storageLocationRepo,
|
||||
Config $config,
|
||||
Environment $environment
|
||||
Environment $environment,
|
||||
Adapters $adapters
|
||||
) {
|
||||
$this->entityClass = Entity\Station::class;
|
||||
$this->station_repo = $station_repo;
|
||||
$this->storageLocationRepo = $storageLocationRepo;
|
||||
$this->environment = $environment;
|
||||
$this->adapters = $adapters;
|
||||
|
||||
$form_config = $config->get('forms/station');
|
||||
$form_config = $config->get(
|
||||
'forms/station',
|
||||
[
|
||||
'adapters' => $adapters,
|
||||
]
|
||||
);
|
||||
parent::__construct($em, $serializer, $validator, $form_config);
|
||||
}
|
||||
|
||||
|
@ -73,7 +82,8 @@ class StationForm extends EntityForm
|
|||
unset($this->options['groups']['admin']);
|
||||
}
|
||||
|
||||
if (!SHOUTcast::isInstalled()) {
|
||||
$installedFrontends = $this->adapters->listFrontendAdapters(true);
|
||||
if (!isset($installedFrontends[Adapters::FRONTEND_SHOUTCAST])) {
|
||||
$frontendDesc = __(
|
||||
'Want to use SHOUTcast 2? <a href="%s" target="_blank">Install it here</a>, then reload this page.',
|
||||
$request->getRouter()->named('admin:install_shoutcast:index')
|
||||
|
|
|
@ -15,7 +15,7 @@ class StationFiles
|
|||
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$backend = $request->getStationBackend();
|
||||
if (!$backend::supportsMedia()) {
|
||||
if (!$backend->supportsMedia()) {
|
||||
throw new Exception(__('This feature is not currently supported on this station.'));
|
||||
}
|
||||
|
||||
|
|
|
@ -18,12 +18,12 @@ use Supervisor\Supervisor;
|
|||
|
||||
abstract class AbstractAdapter
|
||||
{
|
||||
protected Supervisor $supervisor;
|
||||
|
||||
protected Environment $environment;
|
||||
|
||||
protected EntityManagerInterface $em;
|
||||
|
||||
protected Supervisor $supervisor;
|
||||
|
||||
protected EventDispatcher $dispatcher;
|
||||
|
||||
protected LoggerInterface $logger;
|
||||
|
@ -42,31 +42,84 @@ abstract class AbstractAdapter
|
|||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write configuration from Station object to the external service.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
*
|
||||
* @return bool Whether the newly written configuration differs from what was already on disk.
|
||||
*/
|
||||
public function write(Entity\Station $station): bool
|
||||
{
|
||||
return $this->compareConfiguration($station, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the configuration for this adapter as it would exist with current database settings.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
*
|
||||
*/
|
||||
public function getCurrentConfiguration(Entity\Station $station): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the configuration currently generated differs from what is currently stored on disk.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
* @param bool $writeNewConfig
|
||||
*
|
||||
* @return bool Whether configuration generated now differs from what is on disk.
|
||||
*/
|
||||
public function compareConfiguration(Entity\Station $station, bool $writeNewConfig = false): bool
|
||||
{
|
||||
$configPath = $this->getConfigurationPath($station);
|
||||
if (null === $configPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentConfig = (file_exists($configPath))
|
||||
? file_get_contents($configPath)
|
||||
: null;
|
||||
|
||||
$newConfig = $this->getCurrentConfiguration($station);
|
||||
|
||||
if ($writeNewConfig) {
|
||||
file_put_contents($configPath, $newConfig);
|
||||
}
|
||||
|
||||
return 0 !== strcmp($currentConfig, $newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the main path where configuration data is stored for this adapter.
|
||||
*
|
||||
*/
|
||||
public function getConfigurationPath(Entity\Station $station): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate if the adapter in question is installed on the server.
|
||||
*/
|
||||
public static function isInstalled(): bool
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
return (static::getBinary() !== false);
|
||||
return (null !== $this->getBinary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the binary executable location for this item.
|
||||
*
|
||||
* @return string|bool Returns either the path to the binary if it exists or a boolean for error/success
|
||||
* @return string|null Returns either the path to the binary if it exists or null for no binary.
|
||||
*/
|
||||
public static function getBinary()
|
||||
public function getBinary(): ?string
|
||||
{
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write configuration from Station object to the external service.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
*/
|
||||
abstract public function write(Entity\Station $station): bool;
|
||||
|
||||
/**
|
||||
* Check if the service is running.
|
||||
*
|
||||
|
@ -74,16 +127,16 @@ abstract class AbstractAdapter
|
|||
*/
|
||||
public function isRunning(Entity\Station $station): bool
|
||||
{
|
||||
if ($this->hasCommand($station)) {
|
||||
$program_name = $this->getProgramName($station);
|
||||
$process = $this->supervisor->getProcess($program_name);
|
||||
|
||||
if ($process instanceof Process) {
|
||||
return $process->isRunning();
|
||||
}
|
||||
if (!$this->hasCommand($station)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
$program_name = $this->getProgramName($station);
|
||||
$process = $this->supervisor->getProcess($program_name);
|
||||
|
||||
return ($process instanceof Process)
|
||||
? $process->isRunning()
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,14 +170,29 @@ abstract class AbstractAdapter
|
|||
*/
|
||||
abstract public function getProgramName(Entity\Station $station): string;
|
||||
|
||||
public function supportsSoftReload(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-reloads an adapter, if it's supported by the adapter.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
*/
|
||||
public function reload(Entity\Station $station): void
|
||||
{
|
||||
if (!$this->supportsSoftReload()) {
|
||||
$this->restart($station);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Feature not implemented!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the executable service.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
*
|
||||
* @throws SupervisorException
|
||||
* @throws AlreadyRunningException
|
||||
* @throws NotRunningException
|
||||
*/
|
||||
public function restart(Entity\Station $station): void
|
||||
{
|
||||
|
@ -196,7 +264,7 @@ abstract class AbstractAdapter
|
|||
*/
|
||||
protected function handleSupervisorException(
|
||||
SupervisorLibException $e,
|
||||
$program_name,
|
||||
string $program_name,
|
||||
Entity\Station $station
|
||||
): void {
|
||||
$class_parts = explode('\\', static::class);
|
||||
|
|
|
@ -40,7 +40,7 @@ class Adapters
|
|||
*/
|
||||
public function getFrontendAdapter(Entity\Station $station): Frontend\AbstractFrontend
|
||||
{
|
||||
$adapters = self::listFrontendAdapters();
|
||||
$adapters = $this->listFrontendAdapters();
|
||||
|
||||
$frontend_type = $station->getFrontendType();
|
||||
|
||||
|
@ -62,33 +62,32 @@ class Adapters
|
|||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function listFrontendAdapters($check_installed = false): array
|
||||
public function listFrontendAdapters(bool $check_installed = false): array
|
||||
{
|
||||
static $adapters;
|
||||
|
||||
if ($adapters === null) {
|
||||
$adapters = [
|
||||
self::FRONTEND_ICECAST => [
|
||||
'name' => __('Use <b>%s</b> on this server', 'Icecast 2.4'),
|
||||
'class' => Frontend\Icecast::class,
|
||||
],
|
||||
self::FRONTEND_SHOUTCAST => [
|
||||
'name' => __('Use <b>%s</b> on this server', 'SHOUTcast DNAS 2'),
|
||||
'class' => Frontend\SHOUTcast::class,
|
||||
],
|
||||
self::FRONTEND_REMOTE => [
|
||||
'name' => __('Connect to a <b>remote radio server</b>'),
|
||||
'class' => Frontend\Remote::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
$adapters = [
|
||||
self::FRONTEND_ICECAST => [
|
||||
'name' => __('Use <b>%s</b> on this server', 'Icecast 2.4'),
|
||||
'class' => Frontend\Icecast::class,
|
||||
],
|
||||
self::FRONTEND_SHOUTCAST => [
|
||||
'name' => __('Use <b>%s</b> on this server', 'SHOUTcast DNAS 2'),
|
||||
'class' => Frontend\SHOUTcast::class,
|
||||
],
|
||||
self::FRONTEND_REMOTE => [
|
||||
'name' => __('Connect to a <b>remote radio server</b>'),
|
||||
'class' => Frontend\Remote::class,
|
||||
],
|
||||
];
|
||||
|
||||
if ($check_installed) {
|
||||
return array_filter($adapters, function ($adapter_info) {
|
||||
/** @var AbstractAdapter $adapter_class */
|
||||
$adapter_class = $adapter_info['class'];
|
||||
return $adapter_class::isInstalled();
|
||||
});
|
||||
return array_filter(
|
||||
$adapters,
|
||||
function ($adapter_info) {
|
||||
/** @var AbstractAdapter $adapter */
|
||||
$adapter = $this->adapters->get($adapter_info['class']);
|
||||
return $adapter->isInstalled();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $adapters;
|
||||
|
@ -101,7 +100,7 @@ class Adapters
|
|||
*/
|
||||
public function getBackendAdapter(Entity\Station $station): Backend\AbstractBackend
|
||||
{
|
||||
$adapters = self::listBackendAdapters();
|
||||
$adapters = $this->listBackendAdapters();
|
||||
|
||||
$backend_type = $station->getBackendType();
|
||||
|
||||
|
@ -123,29 +122,28 @@ class Adapters
|
|||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function listBackendAdapters($check_installed = false): array
|
||||
public function listBackendAdapters(bool $check_installed = false): array
|
||||
{
|
||||
static $adapters;
|
||||
|
||||
if ($adapters === null) {
|
||||
$adapters = [
|
||||
self::BACKEND_LIQUIDSOAP => [
|
||||
'name' => __('Use <b>%s</b> on this server', 'Liquidsoap'),
|
||||
'class' => Backend\Liquidsoap::class,
|
||||
],
|
||||
self::BACKEND_NONE => [
|
||||
'name' => __('<b>Do not use</b> an AutoDJ service'),
|
||||
'class' => Backend\None::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
$adapters = [
|
||||
self::BACKEND_LIQUIDSOAP => [
|
||||
'name' => __('Use <b>%s</b> on this server', 'Liquidsoap'),
|
||||
'class' => Backend\Liquidsoap::class,
|
||||
],
|
||||
self::BACKEND_NONE => [
|
||||
'name' => __('<b>Do not use</b> an AutoDJ service'),
|
||||
'class' => Backend\None::class,
|
||||
],
|
||||
];
|
||||
|
||||
if ($check_installed) {
|
||||
return array_filter($adapters, function ($adapter_info) {
|
||||
/** @var AbstractAdapter $adapter_class */
|
||||
$adapter_class = $adapter_info['class'];
|
||||
return $adapter_class::isInstalled();
|
||||
});
|
||||
return array_filter(
|
||||
$adapters,
|
||||
function ($adapter_info) {
|
||||
/** @var AbstractAdapter $adapter */
|
||||
$adapter = $this->adapters->get($adapter_info['class']);
|
||||
return $adapter->isInstalled();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $adapters;
|
||||
|
@ -178,7 +176,7 @@ class Adapters
|
|||
*/
|
||||
public function getRemoteAdapter(Entity\Station $station, Entity\StationRemote $remote): Remote\AbstractRemote
|
||||
{
|
||||
$adapters = self::listRemoteAdapters();
|
||||
$adapters = $this->listRemoteAdapters();
|
||||
|
||||
$remote_type = $remote->getType();
|
||||
|
||||
|
@ -198,7 +196,7 @@ class Adapters
|
|||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function listRemoteAdapters(): array
|
||||
public function listRemoteAdapters(): array
|
||||
{
|
||||
return [
|
||||
self::REMOTE_SHOUTCAST1 => [
|
||||
|
|
|
@ -7,24 +7,24 @@ use App\Radio\AbstractAdapter;
|
|||
|
||||
abstract class AbstractBackend extends AbstractAdapter
|
||||
{
|
||||
public static function supportsMedia(): bool
|
||||
public function supportsMedia(): bool
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function supportsRequests(): bool
|
||||
public function supportsRequests(): bool
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function supportsStreamers(): bool
|
||||
public function supportsStreamers(): bool
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function supportsWebStreaming(): bool
|
||||
public function supportsWebStreaming(): bool
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getStreamPort(Entity\Station $station): ?int
|
||||
|
@ -33,6 +33,8 @@ abstract class AbstractBackend extends AbstractAdapter
|
|||
}
|
||||
|
||||
/**
|
||||
* @param Entity\StationMedia $media
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function annotateMedia(Entity\StationMedia $media): array
|
||||
|
|
|
@ -30,30 +30,50 @@ class Liquidsoap extends AbstractBackend
|
|||
$this->streamerRepo = $streamerRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write configuration from Station object to the external service.
|
||||
*
|
||||
* Special thanks to the team of PonyvilleFM for assisting with Liquidsoap configuration and debugging.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
*/
|
||||
public function write(Entity\Station $station): bool
|
||||
public function supportsMedia(): bool
|
||||
{
|
||||
$event = new WriteLiquidsoapConfiguration($station);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$ls_config_contents = $event->buildConfiguration();
|
||||
|
||||
$config_path = $station->getRadioConfigDir();
|
||||
$ls_config_path = $config_path . '/liquidsoap.liq';
|
||||
|
||||
file_put_contents($ls_config_path, $ls_config_contents);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supportsRequests(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supportsStreamers(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supportsWebStreaming(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getConfigurationPath(Entity\Station $station): ?string
|
||||
{
|
||||
return $station->getRadioConfigDir() . '/liquidsoap.liq';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCurrentConfiguration(Entity\Station $station): ?string
|
||||
{
|
||||
return $this->doGetConfiguration($station, false);
|
||||
}
|
||||
|
||||
public function getEditableConfiguration(Entity\Station $station): string
|
||||
{
|
||||
$event = new WriteLiquidsoapConfiguration($station, true);
|
||||
return $this->doGetConfiguration($station, true);
|
||||
}
|
||||
|
||||
protected function doGetConfiguration(Entity\Station $station, bool $forEditing = false): string
|
||||
{
|
||||
$event = new WriteLiquidsoapConfiguration($station, $forEditing);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $event->buildConfiguration();
|
||||
|
@ -208,18 +228,18 @@ class Liquidsoap extends AbstractBackend
|
|||
*/
|
||||
public function getCommand(Entity\Station $station): ?string
|
||||
{
|
||||
if ($binary = self::getBinary()) {
|
||||
if ($binary = $this->getBinary()) {
|
||||
$config_path = $station->getRadioConfigDir() . '/liquidsoap.liq';
|
||||
return $binary . ' ' . $config_path;
|
||||
}
|
||||
|
||||
return '/bin/false';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getBinary()
|
||||
public function getBinary(): ?string
|
||||
{
|
||||
// Docker revisions 3 and later use the `radio` container.
|
||||
$environment = Environment::getInstance();
|
||||
|
|
|
@ -6,36 +6,6 @@ use App\Entity;
|
|||
|
||||
class None extends AbstractBackend
|
||||
{
|
||||
public static function supportsMedia(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function supportsStreamers(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function supportsWebStreaming(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function supportsRequests(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function write(Entity\Station $station): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isRunning(Entity\Station $station): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function start(Entity\Station $station): void
|
||||
{
|
||||
$this->logger->error(
|
||||
|
|
|
@ -39,59 +39,95 @@ class Configuration
|
|||
$this->environment = $environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write all configuration changes to the filesystem and reload supervisord.
|
||||
*
|
||||
* @param Station $station
|
||||
* @param bool $regen_auth_key
|
||||
* @param bool $force_restart Always restart this station's supervisor instances, even if nothing changed.
|
||||
*/
|
||||
public function writeConfiguration(Station $station, $regen_auth_key = false, $force_restart = false): void
|
||||
public function initializeConfiguration(Station $station): void
|
||||
{
|
||||
// Ensure default values for frontend/backend config exist.
|
||||
$station->setFrontendConfig($station->getFrontendConfig());
|
||||
$station->setBackendConfig($station->getBackendConfig());
|
||||
|
||||
// Ensure port configuration exists
|
||||
$this->assignRadioPorts($station);
|
||||
|
||||
// Clear station caches and generate API adapter key if none exists.
|
||||
if (empty($station->getAdapterApiKey())) {
|
||||
$station->generateAdapterApiKey();
|
||||
}
|
||||
|
||||
// Ensure all directories exist.
|
||||
$station->ensureDirectoriesExist();
|
||||
|
||||
$this->em->persist($station);
|
||||
$this->em->persist($station->getMediaStorageLocation());
|
||||
$this->em->persist($station->getRecordingsStorageLocation());
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function handleConfigurationChange(Station $station): void
|
||||
{
|
||||
if ($this->environment->isTesting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize adapters.
|
||||
$supervisor_config = [];
|
||||
$supervisor_config_path = $this->getSupervisorConfigFile($station);
|
||||
$this->initializeConfiguration($station);
|
||||
|
||||
if (!$station->isEnabled()) {
|
||||
@unlink($supervisor_config_path);
|
||||
$this->reloadSupervisorForStation($station, false);
|
||||
if (!$station->isEnabled() || !$station->getHasStarted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure port configuration exists
|
||||
$this->assignRadioPorts($station, false);
|
||||
$frontend = $this->adapters->getFrontendAdapter($station);
|
||||
$backend = $this->adapters->getBackendAdapter($station);
|
||||
|
||||
// Clear station caches and generate API adapter key if none exists.
|
||||
if ($regen_auth_key || empty($station->getAdapterApiKey())) {
|
||||
$station->generateAdapterApiKey();
|
||||
if (!$frontend->hasCommand($station) && !$backend->hasCommand($station)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$station->clearCache();
|
||||
$frontendChanged = $frontend->compareConfiguration($station);
|
||||
$backendChanged = $backend->compareConfiguration($station);
|
||||
|
||||
$this->em->persist($station);
|
||||
$this->em->flush();
|
||||
if ($frontendChanged || $backendChanged) {
|
||||
$station->setNeedsRestart(true);
|
||||
$this->em->persist($station);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write all configuration changes to the filesystem and reload supervisord.
|
||||
*
|
||||
* @param Station $station
|
||||
* @param bool $forceRestart Always restart this station's supervisor instances, even if nothing changed.
|
||||
*/
|
||||
public function writeConfiguration(
|
||||
Station $station,
|
||||
bool $forceRestart = false
|
||||
): void {
|
||||
if ($this->environment->isTesting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->initializeConfiguration($station);
|
||||
|
||||
// Initialize adapters.
|
||||
$supervisorConfig = [];
|
||||
$supervisorConfigFile = $this->getSupervisorConfigFile($station);
|
||||
|
||||
if (!$station->isEnabled()) {
|
||||
@unlink($supervisorConfigFile);
|
||||
$this->reloadSupervisorForStation($station);
|
||||
return;
|
||||
}
|
||||
|
||||
$frontend = $this->adapters->getFrontendAdapter($station);
|
||||
$backend = $this->adapters->getBackendAdapter($station);
|
||||
|
||||
// If no processes need to be managed, remove any existing config.
|
||||
if (!$frontend->hasCommand($station) && !$backend->hasCommand($station)) {
|
||||
@unlink($supervisor_config_path);
|
||||
$this->reloadSupervisorForStation($station, false);
|
||||
@unlink($supervisorConfigFile);
|
||||
$this->reloadSupervisorForStation($station);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure all directories exist.
|
||||
$station->ensureDirectoriesExist();
|
||||
|
||||
// Write config files for both backend and frontend.
|
||||
$frontend->write($station);
|
||||
$backend->write($station);
|
||||
|
||||
// Get group information
|
||||
$backend_name = $backend->getProgramName($station);
|
||||
[$backend_group, $backend_program] = explode(':', $backend_name);
|
||||
|
@ -108,25 +144,29 @@ class Configuration
|
|||
$programs[] = $frontend_program;
|
||||
}
|
||||
|
||||
$supervisor_config[] = '[group:' . $backend_group . ']';
|
||||
$supervisor_config[] = 'programs=' . implode(',', $programs);
|
||||
$supervisor_config[] = '';
|
||||
$supervisorConfig[] = '[group:' . $backend_group . ']';
|
||||
$supervisorConfig[] = 'programs=' . implode(',', $programs);
|
||||
$supervisorConfig[] = '';
|
||||
|
||||
// Write frontend
|
||||
if ($frontend->hasCommand($station)) {
|
||||
$supervisor_config[] = $this->writeConfigurationSection($station, $frontend, 90);
|
||||
$supervisorConfig[] = $this->writeConfigurationSection($station, $frontend, 90);
|
||||
}
|
||||
|
||||
// Write backend
|
||||
if ($backend->hasCommand($station)) {
|
||||
$supervisor_config[] = $this->writeConfigurationSection($station, $backend, 100);
|
||||
$supervisorConfig[] = $this->writeConfigurationSection($station, $backend, 100);
|
||||
}
|
||||
|
||||
// Write config contents
|
||||
$supervisor_config_data = implode("\n", $supervisor_config);
|
||||
file_put_contents($supervisor_config_path, $supervisor_config_data);
|
||||
$supervisor_config_data = implode("\n", $supervisorConfig);
|
||||
file_put_contents($supervisorConfigFile, $supervisor_config_data);
|
||||
|
||||
$this->reloadSupervisorForStation($station, $force_restart);
|
||||
// Write supporting configurations.
|
||||
$frontend->write($station);
|
||||
$backend->write($station);
|
||||
|
||||
$this->reloadSupervisorForStation($station, $forceRestart);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -145,7 +185,7 @@ class Configuration
|
|||
* @param Station $station
|
||||
* @param bool $force_restart
|
||||
*/
|
||||
protected function reloadSupervisorForStation(Station $station, $force_restart = false): void
|
||||
protected function reloadSupervisorForStation(Station $station, $force_restart = false): bool
|
||||
{
|
||||
$station_group = 'station_' . $station->getId();
|
||||
$affected_groups = $this->reloadSupervisor();
|
||||
|
@ -165,10 +205,13 @@ class Configuration
|
|||
if ($was_restarted) {
|
||||
$station->setHasStarted(true);
|
||||
$station->setNeedsRestart(false);
|
||||
$station->clearCache();
|
||||
|
||||
$this->em->persist($station);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
return $was_restarted;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -54,12 +54,28 @@ abstract class AbstractFrontend extends AbstractAdapter
|
|||
$this->settings = $settingsRepo->readSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the station supports multiple mount points per station
|
||||
*/
|
||||
public function supportsMounts(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the station supports enhanced listener detail (per-client records)
|
||||
*/
|
||||
public function supportsListenerDetail(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default mounts when resetting or initializing a station.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function getDefaultMounts(): array
|
||||
public function getDefaultMounts(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
|
@ -72,29 +88,6 @@ abstract class AbstractFrontend extends AbstractAdapter
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the station supports multiple mount points per station
|
||||
*/
|
||||
public static function supportsMounts(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the station supports enhanced listener detail (per-client records)
|
||||
*/
|
||||
public static function supportsListenerDetail(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read configuration from external service to Station object.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
*/
|
||||
abstract public function read(Entity\Station $station): bool;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -156,8 +149,8 @@ abstract class AbstractFrontend extends AbstractAdapter
|
|||
|
||||
if (
|
||||
$use_radio_proxy
|
||||
|| (!$this->environment->isProduction() && !$this->environment->isDocker())
|
||||
|| 'https' === $base_url->getScheme()
|
||||
|| (!$this->environment->isProduction() && !$this->environment->isDocker())
|
||||
) {
|
||||
// Web proxy support.
|
||||
return $base_url
|
||||
|
@ -194,9 +187,11 @@ abstract class AbstractFrontend extends AbstractAdapter
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string|null $custom_config_raw
|
||||
*
|
||||
* @return mixed[]|bool
|
||||
*/
|
||||
protected function processCustomConfig($custom_config_raw)
|
||||
protected function processCustomConfig(?string $custom_config_raw)
|
||||
{
|
||||
$custom_config = [];
|
||||
|
||||
|
@ -210,11 +205,6 @@ abstract class AbstractFrontend extends AbstractAdapter
|
|||
return $custom_config;
|
||||
}
|
||||
|
||||
protected function getRadioPort(Entity\Station $station): int
|
||||
{
|
||||
return (8000 + (($station->getId() - 1) * 10));
|
||||
}
|
||||
|
||||
protected function writeIpBansFile(Entity\Station $station): string
|
||||
{
|
||||
$ips = [];
|
||||
|
|
|
@ -3,10 +3,8 @@
|
|||
namespace App\Radio\Frontend;
|
||||
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Radio\CertificateLocator;
|
||||
use App\Utilities;
|
||||
use App\Xml\Reader;
|
||||
use App\Xml\Writer;
|
||||
use Exception;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
|
@ -21,6 +19,27 @@ class Icecast extends AbstractFrontend
|
|||
public const LOGLEVEL_WARN = 2;
|
||||
public const LOGLEVEL_ERROR = 1;
|
||||
|
||||
public function supportsMounts(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supportsListenerDetail(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supportsSoftReload(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function reload(Entity\Station $station): void
|
||||
{
|
||||
$this->write($station);
|
||||
// TODO: Implement soft-reload functionality.
|
||||
}
|
||||
|
||||
public function getNowPlaying(Entity\Station $station, bool $includeClients = true): Result
|
||||
{
|
||||
$feConfig = $station->getFrontendConfig();
|
||||
|
@ -66,43 +85,15 @@ class Icecast extends AbstractFrontend
|
|||
return $defaultResult;
|
||||
}
|
||||
|
||||
public function read(Entity\Station $station): bool
|
||||
public function getConfigurationPath(Entity\Station $station): ?string
|
||||
{
|
||||
$config = $this->getConfig($station);
|
||||
$station->setFrontendConfigDefaults($this->loadFromConfig($station, $config));
|
||||
return true;
|
||||
return $station->getRadioConfigDir() . '/icecast.xml';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function getConfig(Entity\Station $station): array
|
||||
public function getCurrentConfiguration(Entity\Station $station): ?string
|
||||
{
|
||||
$config_path = $station->getRadioConfigDir();
|
||||
$icecast_path = $config_path . '/icecast.xml';
|
||||
|
||||
$defaults = $this->getDefaults($station);
|
||||
|
||||
if (file_exists($icecast_path)) {
|
||||
$reader = new Reader();
|
||||
$data = $reader->fromFile($icecast_path);
|
||||
|
||||
return self::arrayMergeRecursiveDistinct($defaults, $data);
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/*
|
||||
* Process Management
|
||||
*/
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function getDefaults(Entity\Station $station): array
|
||||
{
|
||||
$config_dir = $station->getRadioConfigDir();
|
||||
$frontendConfig = $station->getFrontendConfig();
|
||||
$configDir = $station->getRadioConfigDir();
|
||||
|
||||
$settingsBaseUrl = $this->settings->getBaseUrl() ?: 'http://localhost';
|
||||
if (strpos($settingsBaseUrl, 'http') !== 0) {
|
||||
|
@ -112,40 +103,38 @@ class Icecast extends AbstractFrontend
|
|||
|
||||
$certPaths = CertificateLocator::findCertificate();
|
||||
|
||||
$defaults = [
|
||||
$config = [
|
||||
'location' => 'AzuraCast',
|
||||
'admin' => 'icemaster@localhost',
|
||||
'hostname' => $baseUrl->getHost(),
|
||||
'limits' => [
|
||||
'clients' => 2500,
|
||||
'clients' => $frontendConfig->getMaxListeners() ?? 2500,
|
||||
'sources' => $station->getMounts()->count(),
|
||||
// 'threadpool' => 5,
|
||||
'queue-size' => 524288,
|
||||
'client-timeout' => 30,
|
||||
'header-timeout' => 15,
|
||||
'source-timeout' => 10,
|
||||
// 'burst-on-connect' => 1,
|
||||
'burst-size' => 65535,
|
||||
],
|
||||
'authentication' => [
|
||||
'source-password' => Utilities\Strings::generatePassword(),
|
||||
'relay-password' => Utilities\Strings::generatePassword(),
|
||||
'source-password' => $frontendConfig->getSourcePassword(),
|
||||
'relay-password' => $frontendConfig->getRelayPassword(),
|
||||
'admin-user' => 'admin',
|
||||
'admin-password' => Utilities\Strings::generatePassword(),
|
||||
'admin-password' => $frontendConfig->getAdminPassword(),
|
||||
],
|
||||
|
||||
'listen-socket' => [
|
||||
'port' => $this->getRadioPort($station),
|
||||
'port' => $frontendConfig->getPort(),
|
||||
],
|
||||
|
||||
'mount' => [],
|
||||
'fileserve' => 1,
|
||||
'paths' => [
|
||||
'basedir' => '/usr/local/share/icecast',
|
||||
'logdir' => $config_dir,
|
||||
'logdir' => $configDir,
|
||||
'webroot' => '/usr/local/share/icecast/web',
|
||||
'adminroot' => '/usr/local/share/icecast/admin',
|
||||
'pidfile' => $config_dir . '/icecast.pid',
|
||||
'pidfile' => $configDir . '/icecast.pid',
|
||||
'alias' => [
|
||||
'@source' => '/',
|
||||
'@dest' => '/status.xsl',
|
||||
|
@ -196,14 +185,14 @@ class Icecast extends AbstractFrontend
|
|||
$mount_conf = $this->processCustomConfig($mount_row->getFrontendConfig());
|
||||
|
||||
if (!empty($mount_conf)) {
|
||||
$mount = self::arrayMergeRecursiveDistinct($mount, $mount_conf);
|
||||
$mount = Utilities\Arrays::arrayMergeRecursiveDistinct($mount, $mount_conf);
|
||||
}
|
||||
}
|
||||
|
||||
if ($mount_row->getRelayUrl()) {
|
||||
$relay_parts = parse_url($mount_row->getRelayUrl());
|
||||
|
||||
$defaults['relay'][] = [
|
||||
$config['relay'][] = [
|
||||
'server' => $relay_parts['host'],
|
||||
'port' => $relay_parts['port'],
|
||||
'mount' => $relay_parts['path'],
|
||||
|
@ -211,160 +200,40 @@ class Icecast extends AbstractFrontend
|
|||
];
|
||||
}
|
||||
|
||||
$defaults['mount'][] = $mount;
|
||||
$config['mount'][] = $mount;
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* array_merge_recursive does indeed merge arrays, but it converts values with duplicate
|
||||
* keys to arrays rather than overwriting the value in the first array with the duplicate
|
||||
* value in the second array, as array_merge does. I.e., with array_merge_recursive,
|
||||
* this happens (documented behavior):
|
||||
*
|
||||
* array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
|
||||
* => array('key' => array('org value', 'new value'));
|
||||
*
|
||||
* array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
|
||||
* Matching keys' values in the second array overwrite those in the first array, as is the
|
||||
* case with array_merge, i.e.:
|
||||
*
|
||||
* array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
|
||||
* => array('key' => array('new value'));
|
||||
*
|
||||
* Parameters are passed by reference, though only for performance reasons. They're not
|
||||
* altered by this function.
|
||||
*
|
||||
* @param array $array1
|
||||
* @param array $array2
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
|
||||
* @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
|
||||
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
|
||||
*/
|
||||
public static function arrayMergeRecursiveDistinct(array &$array1, array &$array2): array
|
||||
{
|
||||
$merged = $array1;
|
||||
foreach ($array2 as $key => &$value) {
|
||||
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
|
||||
$merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
|
||||
} else {
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/*
|
||||
* Configuration
|
||||
*/
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function loadFromConfig(Entity\Station $station, $config): array
|
||||
{
|
||||
$frontend_config = $station->getFrontendConfig();
|
||||
|
||||
return [
|
||||
Entity\StationFrontendConfiguration::CUSTOM_CONFIGURATION => $frontend_config->getCustomConfiguration(),
|
||||
Entity\StationFrontendConfiguration::SOURCE_PASSWORD => $config['authentication']['source-password'],
|
||||
Entity\StationFrontendConfiguration::ADMIN_PASSWORD => $config['authentication']['admin-password'],
|
||||
Entity\StationFrontendConfiguration::RELAY_PASSWORD => $config['authentication']['relay-password'],
|
||||
Entity\StationFrontendConfiguration::STREAMER_PASSWORD => $config['mount'][0]['password'],
|
||||
Entity\StationFrontendConfiguration::MAX_LISTENERS => $config['limits']['clients'],
|
||||
];
|
||||
}
|
||||
|
||||
public function write(Entity\Station $station): bool
|
||||
{
|
||||
$config = $this->getDefaults($station);
|
||||
|
||||
$frontend_config = $station->getFrontendConfig();
|
||||
|
||||
$port = $frontend_config->getPort();
|
||||
if (null !== $port) {
|
||||
$config['listen-socket']['port'] = $port;
|
||||
}
|
||||
|
||||
$sourcePw = $frontend_config->getSourcePassword();
|
||||
if (!empty($sourcePw)) {
|
||||
$config['authentication']['source-password'] = $sourcePw;
|
||||
}
|
||||
|
||||
$adminPw = $frontend_config->getAdminPassword();
|
||||
if (!empty($adminPw)) {
|
||||
$config['authentication']['admin-password'] = $adminPw;
|
||||
}
|
||||
|
||||
$relayPw = $frontend_config->getRelayPassword();
|
||||
if (!empty($relayPw)) {
|
||||
$config['authentication']['relay-password'] = $relayPw;
|
||||
}
|
||||
|
||||
$streamerPw = $frontend_config->getStreamerPassword();
|
||||
if (!empty($streamerPw)) {
|
||||
foreach ($config['mount'] as &$mount) {
|
||||
if (!empty($mount['password'])) {
|
||||
$mount['password'] = $streamerPw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$maxListeners = $frontend_config->getMaxListeners();
|
||||
if (null !== $maxListeners) {
|
||||
$config['limits']['clients'] = $maxListeners;
|
||||
}
|
||||
|
||||
$customConfig = $frontend_config->getCustomConfiguration();
|
||||
$customConfig = $frontendConfig->getCustomConfiguration();
|
||||
if (!empty($customConfig)) {
|
||||
$custom_conf = $this->processCustomConfig($customConfig);
|
||||
if (!empty($custom_conf)) {
|
||||
$config = self::arrayMergeRecursiveDistinct($config, $custom_conf);
|
||||
$config = Utilities\Arrays::arrayMergeRecursiveDistinct($config, $custom_conf);
|
||||
}
|
||||
}
|
||||
|
||||
// Set any unset values back to the DB config.
|
||||
$station->setFrontendConfigDefaults($this->loadFromConfig($station, $config));
|
||||
|
||||
$this->em->persist($station);
|
||||
$this->em->flush();
|
||||
|
||||
$config_path = $station->getRadioConfigDir();
|
||||
$icecast_path = $config_path . '/icecast.xml';
|
||||
|
||||
$writer = new Writer();
|
||||
$icecast_config_str = $writer->toString($config, 'icecast');
|
||||
$configString = (new Writer())->toString($config, 'icecast');
|
||||
|
||||
// Strip the first line (the XML charset)
|
||||
$icecast_config_str = substr($icecast_config_str, strpos($icecast_config_str, "\n") + 1);
|
||||
|
||||
file_put_contents($icecast_path, $icecast_config_str);
|
||||
return true;
|
||||
return substr($configString, strpos($configString, "\n") + 1);
|
||||
}
|
||||
|
||||
public function getCommand(Entity\Station $station): ?string
|
||||
{
|
||||
if ($binary = self::getBinary()) {
|
||||
$config_path = $station->getRadioConfigDir() . '/icecast.xml';
|
||||
return $binary . ' -c ' . $config_path;
|
||||
if ($binary = $this->getBinary()) {
|
||||
return $binary . ' -c ' . $this->getConfigurationPath($station);
|
||||
}
|
||||
return '/bin/false';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getBinary()
|
||||
public function getBinary(): ?string
|
||||
{
|
||||
$new_path = '/usr/local/bin/icecast';
|
||||
$legacy_path = '/usr/bin/icecast2';
|
||||
|
||||
if (Environment::getInstance()->isDocker() || file_exists($new_path)) {
|
||||
if ($this->environment->isDocker() || file_exists($new_path)) {
|
||||
return $new_path;
|
||||
}
|
||||
|
||||
|
@ -372,7 +241,7 @@ class Icecast extends AbstractFrontend
|
|||
return $legacy_path;
|
||||
}
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAdminUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface
|
||||
|
|
|
@ -8,31 +8,6 @@ use Psr\Http\Message\UriInterface;
|
|||
|
||||
class Remote extends AbstractFrontend
|
||||
{
|
||||
public static function supportsMounts(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function supportsListenerDetail(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function read(Entity\Station $station): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function write(Entity\Station $station): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isRunning(Entity\Station $station): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getStreamUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface
|
||||
{
|
||||
return new Uri('');
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
namespace App\Radio\Frontend;
|
||||
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Utilities;
|
||||
use Exception;
|
||||
use NowPlaying\Adapter\AdapterFactory;
|
||||
use NowPlaying\Result\Result;
|
||||
|
@ -13,9 +11,19 @@ use Symfony\Component\Process\Process;
|
|||
|
||||
class SHOUTcast extends AbstractFrontend
|
||||
{
|
||||
public static function getVersion(): ?string
|
||||
public function supportsMounts(): bool
|
||||
{
|
||||
$binary_path = self::getBinary();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supportsListenerDetail(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getVersion(): ?string
|
||||
{
|
||||
$binary_path = $this->getBinary();
|
||||
if (!$binary_path) {
|
||||
return null;
|
||||
}
|
||||
|
@ -34,13 +42,12 @@ class SHOUTcast extends AbstractFrontend
|
|||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getBinary()
|
||||
public function getBinary(): string
|
||||
{
|
||||
$new_path = '/var/azuracast/servers/shoutcast2/sc_serv';
|
||||
|
||||
// Docker versions before 3 included the SC binary across the board.
|
||||
$environment = Environment::getInstance();
|
||||
if ($environment->isDocker() && !$environment->isDockerRevisionAtLeast(3)) {
|
||||
if ($this->environment->isDocker() && !$this->environment->isDockerRevisionAtLeast(3)) {
|
||||
return $new_path;
|
||||
}
|
||||
|
||||
|
@ -95,73 +102,29 @@ class SHOUTcast extends AbstractFrontend
|
|||
return $defaultResult;
|
||||
}
|
||||
|
||||
/*
|
||||
* Process Management
|
||||
*/
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function read(Entity\Station $station): bool
|
||||
public function getConfigurationPath(Entity\Station $station): ?string
|
||||
{
|
||||
$config = $this->getConfig($station);
|
||||
$station->setFrontendConfigDefaults($this->loadFromConfig($config));
|
||||
return true;
|
||||
return $station->getRadioConfigDir() . '/sc_serv.conf';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]|bool Returns either all lines of the config file or false on failure
|
||||
*/
|
||||
protected function getConfig(Entity\Station $station)
|
||||
public function getCurrentConfiguration(Entity\Station $station): ?string
|
||||
{
|
||||
$config_dir = $station->getRadioConfigDir();
|
||||
return @parse_ini_file($config_dir . '/sc_serv.conf', false, INI_SCANNER_RAW);
|
||||
}
|
||||
$configPath = $station->getRadioConfigDir();
|
||||
$frontendConfig = $station->getFrontendConfig();
|
||||
|
||||
/*
|
||||
* Configuration
|
||||
*/
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function loadFromConfig($config): array
|
||||
{
|
||||
return [
|
||||
Entity\StationFrontendConfiguration::PORT => $config['portbase'],
|
||||
Entity\StationFrontendConfiguration::SOURCE_PASSWORD => $config['password'],
|
||||
Entity\StationFrontendConfiguration::ADMIN_PASSWORD => $config['adminpassword'],
|
||||
Entity\StationFrontendConfiguration::MAX_LISTENERS => $config['maxuser'],
|
||||
$config = [
|
||||
'password' => $frontendConfig->getSourcePassword(),
|
||||
'adminpassword' => $frontendConfig->getAdminPassword(),
|
||||
'logfile' => $configPath . '/sc_serv.log',
|
||||
'w3clog' => $configPath . '/sc_w3c.log',
|
||||
'banfile' => $this->writeIpBansFile($station),
|
||||
'ripfile' => $configPath . '/sc_serv.rip',
|
||||
'maxuser' => $frontendConfig->getMaxListeners() ?? 250,
|
||||
'portbase' => $frontendConfig->getPort(),
|
||||
'requirestreamconfigs' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
public function write(Entity\Station $station): bool
|
||||
{
|
||||
$config = $this->getDefaults($station);
|
||||
|
||||
$frontend_config = $station->getFrontendConfig();
|
||||
|
||||
$port = $frontend_config->getPort();
|
||||
if (null !== $port) {
|
||||
$config['portbase'] = $port;
|
||||
}
|
||||
|
||||
$sourcePw = $frontend_config->getSourcePassword();
|
||||
if (!empty($sourcePw)) {
|
||||
$config['password'] = $sourcePw;
|
||||
}
|
||||
|
||||
$adminPw = $frontend_config->getAdminPassword();
|
||||
if (!empty($adminPw)) {
|
||||
$config['adminpassword'] = $adminPw;
|
||||
}
|
||||
|
||||
$maxListeners = $frontend_config->getMaxListeners();
|
||||
if (null !== $maxListeners) {
|
||||
$config['maxuser'] = $maxListeners;
|
||||
}
|
||||
|
||||
$customConfig = $frontend_config->getCustomConfiguration();
|
||||
$customConfig = $frontendConfig->getCustomConfiguration();
|
||||
if (!empty($customConfig)) {
|
||||
$custom_conf = $this->processCustomConfig($customConfig);
|
||||
if (!empty($custom_conf)) {
|
||||
|
@ -185,54 +148,20 @@ class SHOUTcast extends AbstractFrontend
|
|||
}
|
||||
}
|
||||
|
||||
// Set any unset values back to the DB config.
|
||||
$station->setFrontendConfigDefaults($this->loadFromConfig($config));
|
||||
|
||||
$this->em->persist($station);
|
||||
$this->em->flush();
|
||||
|
||||
$config_path = $station->getRadioConfigDir();
|
||||
$sc_path = $config_path . '/sc_serv.conf';
|
||||
|
||||
$sc_file = '';
|
||||
$configFileOutput = '';
|
||||
foreach ($config as $config_key => $config_value) {
|
||||
$sc_file .= $config_key . '=' . str_replace("\n", '', $config_value) . "\n";
|
||||
$configFileOutput .= $config_key . '=' . str_replace("\n", '', $config_value) . "\n";
|
||||
}
|
||||
|
||||
file_put_contents($sc_path, $sc_file);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function getDefaults(Entity\Station $station): array
|
||||
{
|
||||
$config_path = $station->getRadioConfigDir();
|
||||
|
||||
return [
|
||||
'password' => Utilities\Strings::generatePassword(),
|
||||
'adminpassword' => Utilities\Strings::generatePassword(),
|
||||
'logfile' => $config_path . '/sc_serv.log',
|
||||
'w3clog' => $config_path . '/sc_w3c.log',
|
||||
'banfile' => $this->writeIpBansFile($station),
|
||||
'ripfile' => $config_path . '/sc_serv.rip',
|
||||
'maxuser' => 250,
|
||||
'portbase' => $this->getRadioPort($station),
|
||||
'requirestreamconfigs' => 1,
|
||||
];
|
||||
return $configFileOutput;
|
||||
}
|
||||
|
||||
public function getCommand(Entity\Station $station): ?string
|
||||
{
|
||||
if ($binary = self::getBinary()) {
|
||||
$config_path = $station->getRadioConfigDir();
|
||||
$sc_config = $config_path . '/sc_serv.conf';
|
||||
|
||||
return $binary . ' ' . $sc_config;
|
||||
if ($binary = $this->getBinary()) {
|
||||
return $binary . ' ' . $this->getConfigurationPath($station);
|
||||
}
|
||||
|
||||
return '/bin/false';
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAdminUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface
|
||||
|
@ -241,21 +170,4 @@ class SHOUTcast extends AbstractFrontend
|
|||
return $public_url
|
||||
->withPath($public_url->getPath() . '/admin.cgi');
|
||||
}
|
||||
|
||||
protected function getDefaultMountSid(Entity\Station $station): int
|
||||
{
|
||||
$default_sid = null;
|
||||
$sid = 0;
|
||||
foreach ($station->getMounts() as $mount) {
|
||||
/** @var Entity\StationMount $mount */
|
||||
$sid++;
|
||||
|
||||
if ($mount->getIsDefault()) {
|
||||
$default_sid = $sid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $default_sid ?? 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,4 +66,46 @@ class Arrays
|
|||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* array_merge_recursive does indeed merge arrays, but it converts values with duplicate
|
||||
* keys to arrays rather than overwriting the value in the first array with the duplicate
|
||||
* value in the second array, as array_merge does. I.e., with array_merge_recursive,
|
||||
* this happens (documented behavior):
|
||||
*
|
||||
* array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
|
||||
* => array('key' => array('org value', 'new value'));
|
||||
*
|
||||
* array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
|
||||
* Matching keys' values in the second array overwrite those in the first array, as is the
|
||||
* case with array_merge, i.e.:
|
||||
*
|
||||
* array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
|
||||
* => array('key' => array('new value'));
|
||||
*
|
||||
* Parameters are passed by reference, though only for performance reasons. They're not
|
||||
* altered by this function.
|
||||
*
|
||||
* @param array $array1
|
||||
* @param array $array2
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
|
||||
* @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
|
||||
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
|
||||
*/
|
||||
public static function arrayMergeRecursiveDistinct(array &$array1, array &$array2): array
|
||||
{
|
||||
$merged = $array1;
|
||||
foreach ($array2 as $key => &$value) {
|
||||
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
|
||||
$merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
|
||||
} else {
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ $props = [
|
|||
// Common
|
||||
'backendType' => $station->getBackendType(),
|
||||
'frontendType' => $station->getFrontendType(),
|
||||
'stationSupportsRequests' => $backend::supportsRequests(),
|
||||
'stationSupportsStreamers' => $backend::supportsStreamers(),
|
||||
'stationSupportsRequests' => $backend->supportsRequests(),
|
||||
'stationSupportsStreamers' => $backend->supportsStreamers(),
|
||||
'enableRequests' => $station->getEnableRequests(),
|
||||
'enableStreamers' => $station->getEnableStreamers(),
|
||||
'enablePublicPage' => $station->getEnablePublicPage(),
|
||||
|
|
Loading…
Reference in New Issue