diff --git a/config/menus/station.php b/config/menus/station.php index 96ce0cffe..4a4010726 100644 --- a/config/menus/station.php +++ b/config/menus/station.php @@ -13,13 +13,14 @@ return function (App\Event\BuildStationMenu $e) { $backendConfig = $station->getBackendConfig(); $router = $request->getRouter(); - $backend = $request->getStationBackend(); - $frontend = $request->getStationFrontend(); + + $backendEnum = $station->getBackendTypeEnum(); + $frontendEnum = $station->getFrontendTypeEnum(); $willDisconnectMessage = __('Restart broadcasting? This will disconnect any current listeners.'); $willNotDisconnectMessage = __('Reload broadcasting? Current listeners will not be disconnected.'); - $reloadSupported = $frontend->supportsReload(); + $reloadSupported = $frontendEnum->supportsReload(); $reloadMessage = $reloadSupported ? $willNotDisconnectMessage : $willDisconnectMessage; $settings = $e->getSettings(); @@ -67,28 +68,28 @@ return function (App\Event\BuildStationMenu $e) { 'label' => __('Music Files'), 'icon' => 'library_music', 'url' => (string)$router->fromHere('stations:files:index'), - 'visible' => $backend->supportsMedia(), + 'visible' => $backendEnum->isEnabled(), 'permission' => StationPermissions::Media, ], 'reports_duplicates' => [ 'label' => __('Duplicate Songs'), 'class' => 'text-muted', 'url' => (string)$router->fromHere('stations:files:index') . '#special:duplicates', - 'visible' => $backend->supportsMedia(), + 'visible' => $backendEnum->isEnabled(), 'permission' => StationPermissions::Media, ], 'reports_unprocessable' => [ 'label' => __('Unprocessable Files'), 'class' => 'text-muted', 'url' => (string)$router->fromHere('stations:files:index') . '#special:unprocessable', - 'visible' => $backend->supportsMedia(), + 'visible' => $backendEnum->isEnabled(), 'permission' => StationPermissions::Media, ], 'reports_unassigned' => [ 'label' => __('Unassigned Files'), 'class' => 'text-muted', 'url' => (string)$router->fromHere('stations:files:index') . '#special:unassigned', - 'visible' => $backend->supportsMedia(), + 'visible' => $backendEnum->isEnabled(), 'permission' => StationPermissions::Media, ], 'ondemand' => [ @@ -110,7 +111,7 @@ return function (App\Event\BuildStationMenu $e) { 'label' => __('Bulk Media Import/Export'), 'class' => 'text-muted', 'url' => (string)$router->fromHere('stations:bulk-media'), - 'visible' => $backend->supportsMedia(), + 'visible' => $backendEnum->isEnabled(), 'permission' => StationPermissions::Media, ], ], @@ -120,7 +121,7 @@ return function (App\Event\BuildStationMenu $e) { 'label' => __('Playlists'), 'icon' => 'queue_music', 'url' => (string)$router->fromHere('stations:playlists:index'), - 'visible' => $backend->supportsMedia(), + 'visible' => $backendEnum->isEnabled(), 'permission' => StationPermissions::Media, ], @@ -139,7 +140,7 @@ return function (App\Event\BuildStationMenu $e) { 'label' => __('Streamer/DJ Accounts'), 'icon' => 'mic', 'url' => (string)$router->fromHere('stations:streamers:index'), - 'visible' => $backend->supportsStreamers() && $station->getEnableStreamers(), + 'visible' => $backendEnum->isEnabled() && $station->getEnableStreamers(), 'permission' => StationPermissions::Streamers, ], @@ -203,13 +204,13 @@ return function (App\Event\BuildStationMenu $e) { 'label' => __('Mount Points'), 'icon' => 'wifi_tethering', 'url' => (string)$router->fromHere('stations:mounts:index'), - 'visible' => $frontend->supportsMounts(), + 'visible' => $frontendEnum->supportsMounts(), 'permission' => StationPermissions::MountPoints, ], 'hls_streams' => [ 'label' => __('HLS Streams'), 'url' => (string)$router->fromHere('stations:hls_streams:index'), - 'visible' => $backend->supportsHls() && $station->getEnableHls(), + 'visible' => $backendEnum->isEnabled() && $station->getEnableHls(), 'permission' => StationPermissions::MountPoints, ], 'remotes' => [ @@ -228,8 +229,7 @@ return function (App\Event\BuildStationMenu $e) { 'label' => __('Edit Liquidsoap Configuration'), 'class' => 'text-muted', 'url' => (string)$router->fromHere('stations:util:ls_config'), - 'visible' => $settings->getEnableAdvancedFeatures() - && $backend instanceof App\Radio\Backend\Liquidsoap, + 'visible' => $settings->getEnableAdvancedFeatures() && $backendEnum->isEnabled(), 'permission' => StationPermissions::Broadcasting, ], 'stations:stereo_tool_config' => [ @@ -237,7 +237,7 @@ return function (App\Event\BuildStationMenu $e) { 'class' => 'text-muted', 'url' => (string)$router->fromHere('stations:stereo_tool_config'), 'visible' => $settings->getEnableAdvancedFeatures() - && $backend instanceof App\Radio\Backend\Liquidsoap + && App\Radio\Enums\BackendAdapters::Liquidsoap === $backendEnum && AudioProcessingMethods::StereoTool === $backendConfig->getAudioProcessingMethodEnum(), 'permission' => StationPermissions::Broadcasting, ], diff --git a/src/Console/Command/Internal/OnSslRenewal.php b/src/Console/Command/Internal/OnSslRenewal.php index e474c7e64..be31afc8c 100644 --- a/src/Console/Command/Internal/OnSslRenewal.php +++ b/src/Console/Command/Internal/OnSslRenewal.php @@ -35,10 +35,12 @@ class OnSslRenewal extends CommandAbstract foreach ($stations as $station) { /** @var Entity\Station $station */ - $frontend = $this->adapters->getFrontendAdapter($station); - if ($frontend->supportsReload()) { - $frontend->write($station); - $frontend->reload($station); + if ($station->getFrontendTypeEnum()->supportsReload()) { + $frontend = $this->adapters->getFrontendAdapter($station); + if (null !== $frontend) { + $frontend->write($station); + $frontend->reload($station); + } } } diff --git a/src/Controller/Admin/Debug/TelnetAction.php b/src/Controller/Admin/Debug/TelnetAction.php index d0d38aed8..e1d1f4e5a 100644 --- a/src/Controller/Admin/Debug/TelnetAction.php +++ b/src/Controller/Admin/Debug/TelnetAction.php @@ -4,9 +4,10 @@ declare(strict_types=1); namespace App\Controller\Admin\Debug; +use App\Exception\StationUnsupportedException; use App\Http\Response; use App\Http\ServerRequest; -use App\Radio\Backend\Liquidsoap; +use App\Radio\Adapters; use Monolog\Handler\TestHandler; use Monolog\Level; use Monolog\Logger; @@ -15,7 +16,8 @@ use Psr\Http\Message\ResponseInterface; final class TelnetAction { public function __construct( - private readonly Logger $logger + private readonly Logger $logger, + private readonly Adapters $adapters ) { } @@ -28,20 +30,22 @@ final class TelnetAction $this->logger->pushHandler($testHandler); $station = $request->getStation(); - $backend = $request->getStationBackend(); + $backend = $this->adapters->getBackendAdapter($station); - if ($backend instanceof Liquidsoap) { - $command = $request->getParam('command'); - - $telnetResponse = $backend->command($station, $command); - $this->logger->debug( - 'Telnet Command Response', - [ - 'response' => $telnetResponse, - ] - ); + if (null === $backend) { + throw new StationUnsupportedException(); } + $command = $request->getParam('command'); + + $telnetResponse = $backend->command($station, $command); + $this->logger->debug( + 'Telnet Command Response', + [ + 'response' => $telnetResponse, + ] + ); + $this->logger->popHandler(); return $request->getView()->renderToResponse( diff --git a/src/Controller/Api/Admin/RelaysController.php b/src/Controller/Api/Admin/RelaysController.php index 6d6ac8b93..3bc5b8ad7 100644 --- a/src/Controller/Api/Admin/RelaysController.php +++ b/src/Controller/Api/Admin/RelaysController.php @@ -52,8 +52,6 @@ final class RelaysController $return = []; foreach ($stations as $station) { - $fa = $this->adapters->getFrontendAdapter($station); - $row = new Entity\Api\Admin\Relay(); $row->id = $station->getIdRequired(); $row->name = $station->getName(); @@ -70,7 +68,9 @@ final class RelaysController $row->admin_pw = $frontend_config->getAdminPassword(); $mounts = []; - if ($station->getMounts()->count() > 0) { + + $fa = $this->adapters->getFrontendAdapter($station); + if (null !== $fa && $station->getMounts()->count() > 0) { foreach ($station->getMounts() as $mount) { /** @var Entity\StationMount $mount */ $mounts[] = $mount->api($fa); diff --git a/src/Controller/Api/Admin/Stations/CloneAction.php b/src/Controller/Api/Admin/Stations/CloneAction.php index 8a0153709..51369779e 100644 --- a/src/Controller/Api/Admin/Stations/CloneAction.php +++ b/src/Controller/Api/Admin/Stations/CloneAction.php @@ -10,7 +10,6 @@ use App\Entity; use App\Environment; use App\Http\Response; use App\Http\ServerRequest; -use App\Radio\Adapters; use App\Radio\Configuration; use DeepCopy; use Doctrine\Common\Collections\Collection; @@ -36,7 +35,6 @@ final class CloneAction extends StationsController Entity\Repository\StationRepository $stationRepo, Entity\Repository\StorageLocationRepository $storageLocationRepo, Entity\Repository\StationQueueRepository $queueRepo, - Adapters $adapters, Configuration $configuration, ReloadableEntityManagerInterface $reloadableEm, Serializer $serializer, @@ -47,7 +45,6 @@ final class CloneAction extends StationsController $stationRepo, $storageLocationRepo, $queueRepo, - $adapters, $configuration, $reloadableEm, $serializer, @@ -164,10 +161,7 @@ final class CloneAction extends StationsController $this->cloneCollection($record->getMounts(), $newStation, $copier); } else { $newStation = $this->reloadableEm->refetch($newStation); - - // Create default mountpoints if station supports them. - $frontendAdapter = $this->adapters->getFrontendAdapter($newStation); - $this->stationRepo->resetMounts($newStation, $frontendAdapter); + $this->stationRepo->resetMounts($newStation); } if (in_array(self::CLONE_REMOTES, $toClone, true)) { diff --git a/src/Controller/Api/Admin/StationsController.php b/src/Controller/Api/Admin/StationsController.php index b2f59e4b8..630af1ca6 100644 --- a/src/Controller/Api/Admin/StationsController.php +++ b/src/Controller/Api/Admin/StationsController.php @@ -11,7 +11,6 @@ use App\Exception\ValidationException; use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; -use App\Radio\Adapters; use App\Radio\Configuration; use InvalidArgumentException; use OpenApi\Attributes as OA; @@ -147,7 +146,6 @@ class StationsController extends AbstractAdminApiCrudController protected Entity\Repository\StationRepository $stationRepo, protected Entity\Repository\StorageLocationRepository $storageLocationRepo, protected Entity\Repository\StationQueueRepository $queueRepo, - protected Adapters $adapters, protected Configuration $configuration, protected ReloadableEntityManagerInterface $reloadableEm, Serializer $serializer, @@ -323,13 +321,11 @@ class StationsController extends AbstractAdminApiCrudController $hls_changed = $old_hls !== $station->getEnableHls(); if ($frontend_changed) { - $frontend = $this->adapters->getFrontendAdapter($station); - $this->stationRepo->resetMounts($station, $frontend); + $this->stationRepo->resetMounts($station); } if ($hls_changed || $backend_changed) { - $backend = $this->adapters->getBackendAdapter($station); - $this->stationRepo->resetHls($station, $backend); + $this->stationRepo->resetHls($station); } if ($adapter_changed || !$station->getIsEnabled()) { @@ -355,8 +351,7 @@ class StationsController extends AbstractAdminApiCrudController $this->configuration->initializeConfiguration($station); // Create default mountpoints if station supports them. - $frontend_adapter = $this->adapters->getFrontendAdapter($station); - $this->stationRepo->resetMounts($station, $frontend_adapter); + $this->stationRepo->resetMounts($station); return $station; } diff --git a/src/Controller/Api/Stations/Files/BatchAction.php b/src/Controller/Api/Stations/Files/BatchAction.php index beed49408..23c9d611e 100644 --- a/src/Controller/Api/Stations/Files/BatchAction.php +++ b/src/Controller/Api/Stations/Files/BatchAction.php @@ -101,7 +101,7 @@ final class BatchAction $fs ); - $this->writePlaylistChanges($request, $affectedPlaylists); + $this->writePlaylistChanges($station, $affectedPlaylists); return $result; } @@ -193,7 +193,7 @@ final class BatchAction $this->em->flush(); - $this->writePlaylistChanges($request, $affectedPlaylists); + $this->writePlaylistChanges($station, $affectedPlaylists); return $result; } @@ -440,13 +440,11 @@ final class BatchAction } private function writePlaylistChanges( - ServerRequest $request, + Entity\Station $station, array $playlists ): void { // Write new PLS playlist configuration. - $backend = $request->getStationBackend(); - - if ($backend instanceof Liquidsoap) { + if ($station->getBackendTypeEnum()->isEnabled()) { foreach ($playlists as $playlistId => $playlistRow) { // Instruct the message queue to start a new "write playlist to file" task. $message = new Message\WritePlaylistFileMessage(); diff --git a/src/Controller/Api/Stations/HlsStreamsController.php b/src/Controller/Api/Stations/HlsStreamsController.php index f89279f5a..98107287e 100644 --- a/src/Controller/Api/Stations/HlsStreamsController.php +++ b/src/Controller/Api/Stations/HlsStreamsController.php @@ -145,8 +145,7 @@ final class HlsStreamsController extends AbstractStationApiCrudController { $station = parent::getStation($request); - $backend = $request->getStationBackend(); - if (!$backend->supportsHls()) { + if (!$station->getBackendTypeEnum()->isEnabled()) { throw new StationUnsupportedException(); } diff --git a/src/Controller/Api/Stations/MountsController.php b/src/Controller/Api/Stations/MountsController.php index 8824abad4..a62c396bc 100644 --- a/src/Controller/Api/Stations/MountsController.php +++ b/src/Controller/Api/Stations/MountsController.php @@ -12,6 +12,7 @@ use App\Http\Response; use App\Http\Router; use App\Http\ServerRequest; use App\OpenApi; +use App\Radio\Adapters; use App\Service\Flow\UploadedFile; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -152,7 +153,8 @@ final class MountsController extends AbstractStationApiCrudController ReloadableEntityManagerInterface $em, Serializer $serializer, ValidatorInterface $validator, - private readonly Entity\Repository\StationMountRepository $mountRepo + private readonly Entity\Repository\StationMountRepository $mountRepo, + private readonly Adapters $adapters, ) { parent::__construct($em, $serializer, $validator); } @@ -195,20 +197,23 @@ final class MountsController extends AbstractStationApiCrudController $return = parent::viewRecord($record, $request); $station = $request->getStation(); - $frontend = $request->getStationFrontend(); $router = $request->getRouter(); + $frontend = $this->adapters->getFrontendAdapter($station); + $return['links']['intro'] = (string)$router->fromHere( route_name: 'api:stations:mounts:intro', route_params: ['id' => $record->getId()], absolute: true ); - $return['links']['listen'] = (string)Router::resolveUri( - $router->getBaseUrl(), - $frontend->getUrlForMount($station, $record), - true - ); + if (null !== $frontend) { + $return['links']['listen'] = (string)Router::resolveUri( + $router->getBaseUrl(), + $frontend->getUrlForMount($station, $record), + true + ); + } return $return; } @@ -259,8 +264,7 @@ final class MountsController extends AbstractStationApiCrudController { $station = parent::getStation($request); - $frontend = $request->getStationFrontend(); - if (!$frontend->supportsMounts()) { + if (!$station->getFrontendTypeEnum()->supportsMounts()) { throw new StationUnsupportedException(); } diff --git a/src/Controller/Api/Stations/ProfileAction.php b/src/Controller/Api/Stations/ProfileAction.php index bebb15833..57eb9b6b9 100644 --- a/src/Controller/Api/Stations/ProfileAction.php +++ b/src/Controller/Api/Stations/ProfileAction.php @@ -7,6 +7,7 @@ namespace App\Controller\Api\Stations; use App\Entity; use App\Http\Response; use App\Http\ServerRequest; +use App\Radio\Adapters; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\ResponseInterface; @@ -15,7 +16,8 @@ final class ProfileAction public function __construct( private readonly Entity\Repository\StationScheduleRepository $scheduleRepo, private readonly Entity\ApiGenerator\NowPlayingApiGenerator $nowPlayingApiGenerator, - private readonly Entity\ApiGenerator\StationApiGenerator $stationApiGenerator + private readonly Entity\ApiGenerator\StationApiGenerator $stationApiGenerator, + private readonly Adapters $adapters, ) { } @@ -25,8 +27,8 @@ final class ProfileAction string $station_id ): ResponseInterface { $station = $request->getStation(); - $backend = $request->getStationBackend(); - $frontend = $request->getStationFrontend(); + $backend = $this->adapters->getBackendAdapter($station); + $frontend = $this->adapters->getFrontendAdapter($station); $baseUri = new Uri(''); $nowPlayingApi = $this->nowPlayingApiGenerator->currentOrEmpty($station, $baseUri); @@ -38,8 +40,8 @@ final class ProfileAction $apiResponse->cache = 'database'; $apiResponse->services = new Entity\Api\StationServiceStatus( - $backend->isRunning($station), - $frontend->isRunning($station), + null !== $backend && $backend->isRunning($station), + null !== $frontend && $frontend->isRunning($station), $station->getHasStarted(), $station->getNeedsRestart() ); diff --git a/src/Controller/Api/Stations/RequestsController.php b/src/Controller/Api/Stations/RequestsController.php index c5c11e11c..633c6a679 100644 --- a/src/Controller/Api/Stations/RequestsController.php +++ b/src/Controller/Api/Stations/RequestsController.php @@ -81,8 +81,7 @@ final class RequestsController $station = $request->getStation(); // Verify that the station supports requests. - $ba = $request->getStationBackend(); - if (!$ba->supportsRequests() || !$station->getEnableRequests()) { + if (!$station->getBackendTypeEnum()->isEnabled() || !$station->getEnableRequests()) { return $response->withStatus(403) ->withJson(new Entity\Api\Error(403, __('This station does not accept requests currently.'))); } @@ -190,8 +189,7 @@ final class RequestsController $station = $request->getStation(); // Verify that the station supports requests. - $ba = $request->getStationBackend(); - if (!$ba->supportsRequests() || !$station->getEnableRequests()) { + if (!$station->getBackendTypeEnum()->isEnabled() || !$station->getEnableRequests()) { return $response->withStatus(403) ->withJson(new Entity\Api\Error(403, __('This station does not accept requests currently.'))); } diff --git a/src/Controller/Api/Stations/ServicesController.php b/src/Controller/Api/Stations/ServicesController.php index 753b6b878..aeb8b61e4 100644 --- a/src/Controller/Api/Stations/ServicesController.php +++ b/src/Controller/Api/Stations/ServicesController.php @@ -5,12 +5,13 @@ declare(strict_types=1); namespace App\Controller\Api\Stations; use App\Entity; +use App\Exception\StationUnsupportedException; use App\Exception\Supervisor\NotRunningException; use App\Http\Response; use App\Http\ServerRequest; use App\Nginx\Nginx; use App\OpenApi; -use App\Radio\Backend\Liquidsoap; +use App\Radio\Adapters; use App\Radio\Configuration; use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; @@ -109,6 +110,7 @@ final class ServicesController private readonly EntityManagerInterface $em, private readonly Configuration $configuration, private readonly Nginx $nginx, + private readonly Adapters $adapters, ) { } @@ -119,13 +121,13 @@ final class ServicesController ): ResponseInterface { $station = $request->getStation(); - $backend = $request->getStationBackend(); - $frontend = $request->getStationFrontend(); + $backend = $this->adapters->getBackendAdapter($station); + $frontend = $this->adapters->getFrontendAdapter($station); return $response->withJson( new Entity\Api\StationServiceStatus( - $backend->isRunning($station), - $frontend->isRunning($station), + null !== $backend && $backend->isRunning($station), + null !== $frontend && $frontend->isRunning($station), $station->getHasStarted(), $station->getNeedsRestart() ) @@ -202,7 +204,11 @@ final class ServicesController string $do = 'restart' ): ResponseInterface { $station = $request->getStation(); - $frontend = $request->getStationFrontend(); + $frontend = $this->adapters->getFrontendAdapter($station); + + if (null === $frontend) { + throw new StationUnsupportedException(); + } switch ($do) { case 'stop': @@ -242,20 +248,20 @@ final class ServicesController string $do = 'restart' ): ResponseInterface { $station = $request->getStation(); - $backend = $request->getStationBackend(); + $backend = $this->adapters->getBackendAdapter($station); + + if (null === $backend) { + throw new StationUnsupportedException(); + } switch ($do) { case 'skip': - if ($backend instanceof Liquidsoap) { - $backend->skip($station); - } + $backend->skip($station); return $response->withJson(new Entity\Api\Status(true, __('Song skipped.'))); case 'disconnect': - if ($backend instanceof Liquidsoap) { - $backend->disconnectStreamer($station); - } + $backend->disconnectStreamer($station); return $response->withJson(new Entity\Api\Status(true, __('Streamer disconnected.'))); diff --git a/src/Controller/Api/Stations/StreamersController.php b/src/Controller/Api/Stations/StreamersController.php index fb39c9331..b58beaf75 100644 --- a/src/Controller/Api/Stations/StreamersController.php +++ b/src/Controller/Api/Stations/StreamersController.php @@ -311,8 +311,7 @@ final class StreamersController extends AbstractScheduledEntityController { $station = parent::getStation($request); - $backend = $request->getStationBackend(); - if (!$backend->supportsStreamers()) { + if (!$station->getBackendTypeEnum()->isEnabled()) { throw new StationUnsupportedException(); } diff --git a/src/Controller/Api/Stations/UpdateMetadataAction.php b/src/Controller/Api/Stations/UpdateMetadataAction.php index c93609976..3b98e44ba 100644 --- a/src/Controller/Api/Stations/UpdateMetadataAction.php +++ b/src/Controller/Api/Stations/UpdateMetadataAction.php @@ -5,25 +5,32 @@ declare(strict_types=1); namespace App\Controller\Api\Stations; use App\Entity; +use App\Exception\StationUnsupportedException; use App\Http\Response; use App\Http\ServerRequest; +use App\Radio\Adapters; use Psr\Http\Message\ResponseInterface; use const ARRAY_FILTER_USE_KEY; final class UpdateMetadataAction { + public function __construct( + private readonly Adapters $adapters, + ) { + } + public function __invoke( ServerRequest $request, Response $response, string $station_id ): ResponseInterface { $station = $request->getStation(); - $backend = $request->getStationBackend(); - if (!method_exists($backend, 'updateMetadata')) { - return $response->withStatus(500) - ->withJson(new Entity\Api\Error(500, 'This function is not supported on this station.')); + $backend = $this->adapters->getBackendAdapter($station); + + if (null === $backend) { + throw new StationUnsupportedException(); } $allowedMetaFields = [ diff --git a/src/Controller/Frontend/PublicPages/PlaylistAction.php b/src/Controller/Frontend/PublicPages/PlaylistAction.php index f45c742bd..4c9cdcef2 100644 --- a/src/Controller/Frontend/PublicPages/PlaylistAction.php +++ b/src/Controller/Frontend/PublicPages/PlaylistAction.php @@ -7,10 +7,16 @@ namespace App\Controller\Frontend\PublicPages; use App\Entity; use App\Http\Response; use App\Http\ServerRequest; +use App\Radio\Adapters; use Psr\Http\Message\ResponseInterface; final class PlaylistAction { + public function __construct( + private readonly Adapters $adapters, + ) { + } + public function __invoke( ServerRequest $request, Response $response, @@ -22,31 +28,31 @@ final class PlaylistAction $streams = []; $stream_urls = []; - $fa = $request->getStationFrontend(); - foreach ($station->getMounts() as $mount) { - /** @var Entity\StationMount $mount */ - if (!$mount->getIsVisibleOnPublicPages()) { - continue; + $fa = $this->adapters->getFrontendAdapter($station); + if (null !== $fa) { + foreach ($station->getMounts() as $mount) { + /** @var Entity\StationMount $mount */ + if (!$mount->getIsVisibleOnPublicPages()) { + continue; + } + + $stream_url = $fa->getUrlForMount($station, $mount); + + $stream_urls[] = $stream_url; + $streams[] = [ + 'name' => $station->getName() . ' - ' . $mount->getDisplayName(), + 'url' => $stream_url, + ]; } - - $stream_url = $fa->getUrlForMount($station, $mount); - - $stream_urls[] = $stream_url; - $streams[] = [ - 'name' => $station->getName() . ' - ' . $mount->getDisplayName(), - 'url' => $stream_url, - ]; } - foreach ($request->getStationRemotes() as $remote_proxy) { - $adapter = $remote_proxy->getAdapter(); - $remote = $remote_proxy->getRemote(); - + foreach ($station->getRemotes() as $remote) { if (!$remote->getIsVisibleOnPublicPages()) { continue; } - $stream_url = $adapter->getPublicUrl($remote); + $stream_url = $this->adapters->getRemoteAdapter($station, $remote) + ->getPublicUrl($remote); $stream_urls[] = $stream_url; $streams[] = [ diff --git a/src/Controller/Frontend/PublicPages/WebDjAction.php b/src/Controller/Frontend/PublicPages/WebDjAction.php index 7861652b7..0a650486f 100644 --- a/src/Controller/Frontend/PublicPages/WebDjAction.php +++ b/src/Controller/Frontend/PublicPages/WebDjAction.php @@ -9,13 +9,14 @@ use App\Exception\StationNotFoundException; use App\Exception\StationUnsupportedException; use App\Http\Response; use App\Http\ServerRequest; -use App\Radio\Backend\Liquidsoap; +use App\Radio\Adapters; use Psr\Http\Message\ResponseInterface; final class WebDjAction { public function __construct( - private readonly Assets $assets + private readonly Assets $assets, + private readonly Adapters $adapters, ) { } @@ -34,9 +35,8 @@ final class WebDjAction throw new StationUnsupportedException(); } - $backend = $request->getStationBackend(); - - if (!($backend instanceof Liquidsoap)) { + $backend = $this->adapters->getBackendAdapter($station); + if (null === $backend) { throw new StationUnsupportedException(); } diff --git a/src/Controller/Stations/EditLiquidsoapConfigAction.php b/src/Controller/Stations/EditLiquidsoapConfigAction.php index 802595b8c..6753349ef 100644 --- a/src/Controller/Stations/EditLiquidsoapConfigAction.php +++ b/src/Controller/Stations/EditLiquidsoapConfigAction.php @@ -27,8 +27,7 @@ final class EditLiquidsoapConfigAction ): ResponseInterface { $station = $request->getStation(); - $backend = $request->getStationBackend(); - if (!($backend instanceof Liquidsoap)) { + if (!$station->getBackendTypeEnum()->isEnabled()) { throw new StationUnsupportedException(); } diff --git a/src/Controller/Stations/FilesAction.php b/src/Controller/Stations/FilesAction.php index 999b71349..6b357320f 100644 --- a/src/Controller/Stations/FilesAction.php +++ b/src/Controller/Stations/FilesAction.php @@ -40,7 +40,7 @@ final class FilesAction $router = $request->getRouter(); - $backend = $request->getStationBackend(); + $backendEnum = $station->getBackendTypeEnum(); return $request->getView()->renderVuePage( response: $response, @@ -63,7 +63,7 @@ final class FilesAction 'stationTimeZone' => $station->getTimezone(), 'showSftp' => SftpGo::isSupportedForStation($station), 'sftpUrl' => (string)$router->fromHere('stations:sftp_users:index'), - 'supportsImmediateQueue' => $backend->supportsImmediateQueue(), + 'supportsImmediateQueue' => $backendEnum->isEnabled(), ], ); } diff --git a/src/Controller/Stations/HlsStreamsAction.php b/src/Controller/Stations/HlsStreamsAction.php index 939f7b78e..a4892aef4 100644 --- a/src/Controller/Stations/HlsStreamsAction.php +++ b/src/Controller/Stations/HlsStreamsAction.php @@ -17,9 +17,8 @@ final class HlsStreamsAction string $station_id ): ResponseInterface { $station = $request->getStation(); - $backend = $request->getStationBackend(); - if (!$backend->supportsHls() || !$station->getEnableHls()) { + if (!$station->getBackendTypeEnum()->isEnabled() || !$station->getEnableHls()) { throw new StationUnsupportedException(); } diff --git a/src/Controller/Stations/PlaylistsAction.php b/src/Controller/Stations/PlaylistsAction.php index a170c93ed..b57867c6d 100644 --- a/src/Controller/Stations/PlaylistsAction.php +++ b/src/Controller/Stations/PlaylistsAction.php @@ -24,8 +24,7 @@ final class PlaylistsAction ): ResponseInterface { $station = $request->getStation(); - $backend = $request->getStationBackend(); - if (!$backend->supportsMedia()) { + if (!$station->getBackendTypeEnum()->isEnabled()) { throw new Exception(__('This feature is not currently supported on this station.')); } diff --git a/src/Controller/Stations/ProfileController.php b/src/Controller/Stations/ProfileController.php index f8575fda2..9366fbc19 100644 --- a/src/Controller/Stations/ProfileController.php +++ b/src/Controller/Stations/ProfileController.php @@ -7,6 +7,7 @@ namespace App\Controller\Stations; use App\Enums\StationPermissions; use App\Http\Response; use App\Http\ServerRequest; +use App\Radio\Adapters; use App\VueComponent\StationFormComponent; use Doctrine\ORM\EntityManagerInterface; use Psr\Http\Message\ResponseInterface; @@ -18,6 +19,7 @@ final class ProfileController public function __construct( private readonly EntityManagerInterface $em, private readonly StationFormComponent $stationFormComponent, + private readonly Adapters $adapters, ) { } @@ -57,9 +59,9 @@ final class ProfileController $csrf = $request->getCsrf()->generate(self::CSRF_NAMESPACE); - $backend = $request->getStationBackend(); - $frontend = $request->getStationFrontend(); + $backendEnum = $station->getBackendTypeEnum(); + $frontend = $this->adapters->getFrontendAdapter($station); $frontendConfig = $station->getFrontendConfig(); $acl = $request->getAcl(); @@ -75,8 +77,8 @@ final class ProfileController 'backendType' => $station->getBackendType(), 'frontendType' => $station->getFrontendType(), 'stationTimeZone' => $station->getTimezone(), - 'stationSupportsRequests' => $backend->supportsRequests(), - 'stationSupportsStreamers' => $backend->supportsStreamers(), + 'stationSupportsRequests' => $backendEnum->isEnabled(), + 'stationSupportsStreamers' => $backendEnum->isEnabled(), 'enableRequests' => $station->getEnableRequests(), 'enableStreamers' => $station->getEnableStreamers(), 'enablePublicPage' => $station->getEnablePublicPage(), @@ -175,7 +177,7 @@ final class ProfileController ), // Frontend - 'frontendAdminUri' => (string)$frontend->getAdminUrl($station, $router->getBaseUrl()), + 'frontendAdminUri' => (string)$frontend?->getAdminUrl($station, $router->getBaseUrl()), 'frontendAdminPassword' => $frontendConfig->getAdminPassword(), 'frontendSourcePassword' => $frontendConfig->getSourcePassword(), 'frontendRelayPassword' => $frontendConfig->getRelayPassword(), diff --git a/src/Controller/Stations/StreamersAction.php b/src/Controller/Stations/StreamersAction.php index 1d3415f01..bb0eda76f 100644 --- a/src/Controller/Stations/StreamersAction.php +++ b/src/Controller/Stations/StreamersAction.php @@ -8,6 +8,7 @@ use App\Entity; use App\Exception\StationUnsupportedException; use App\Http\Response; use App\Http\ServerRequest; +use App\Radio\Adapters; use App\Service\AzuraCastCentral; use Psr\Http\Message\ResponseInterface; @@ -15,7 +16,8 @@ final class StreamersAction { public function __construct( private readonly AzuraCastCentral $acCentral, - private readonly Entity\Repository\SettingsRepository $settingsRepo + private readonly Entity\Repository\SettingsRepository $settingsRepo, + private readonly Adapters $adapters, ) { } @@ -25,9 +27,9 @@ final class StreamersAction string $station_id ): ResponseInterface { $station = $request->getStation(); - $backend = $request->getStationBackend(); + $backend = $this->adapters->getBackendAdapter($station); - if (!$backend->supportsStreamers() && !$station->getEnableStreamers()) { + if (null === $backend || !$station->getEnableStreamers()) { throw new StationUnsupportedException(); } diff --git a/src/Entity/Api/NowPlaying/Station.php b/src/Entity/Api/NowPlaying/Station.php index 890169623..67f7ff46a 100644 --- a/src/Entity/Api/NowPlaying/Station.php +++ b/src/Entity/Api/NowPlaying/Station.php @@ -55,7 +55,7 @@ class Station implements ResolvableUrlInterface description: 'The full URL to listen to the default mount of the station', example: 'http://localhost:8000/radio.mp3' )] - public string|UriInterface $listen_url; + public string|UriInterface|null $listen_url; #[OA\Property( description: 'The public URL of the station.', @@ -115,7 +115,9 @@ class Station implements ResolvableUrlInterface */ public function resolveUrls(UriInterface $base): void { - $this->listen_url = (string)Router::resolveUri($base, $this->listen_url, true); + $this->listen_url = (null !== $this->listen_url) + ? (string)Router::resolveUri($base, $this->listen_url, true) + : null; $this->public_player_url = (string)Router::resolveUri($base, $this->public_player_url, true); $this->playlist_pls_url = (string)Router::resolveUri($base, $this->playlist_pls_url, true); diff --git a/src/Entity/ApiGenerator/StationApiGenerator.php b/src/Entity/ApiGenerator/StationApiGenerator.php index 3743b37fe..2946b5d9e 100644 --- a/src/Entity/ApiGenerator/StationApiGenerator.php +++ b/src/Entity/ApiGenerator/StationApiGenerator.php @@ -22,9 +22,8 @@ class StationApiGenerator ?UriInterface $baseUri = null, bool $showAllMounts = false ): Entity\Api\NowPlaying\Station { - $fa = $this->adapters->getFrontendAdapter($station); + $frontend = $this->adapters->getFrontendAdapter($station); $backend = $this->adapters->getBackendAdapter($station); - $remoteAdapters = $this->adapters->getRemoteAdapters($station); $response = new Entity\Api\NowPlaying\Station(); $response->id = (int)$station->getId(); @@ -35,7 +34,7 @@ class StationApiGenerator $response->backend = (string)$station->getBackendType(); $response->url = $station->getUrl(); $response->is_public = $station->getEnablePublicPage(); - $response->listen_url = $fa->getStreamUrl($station, $baseUri); + $response->listen_url = $frontend?->getStreamUrl($station, $baseUri); $response->public_player_url = (string)$this->router->named( 'public:index', @@ -51,26 +50,31 @@ class StationApiGenerator ); $mounts = []; - if ($fa->supportsMounts() && $station->getMounts()->count() > 0) { + if ( + null !== $frontend && $station->getFrontendTypeEnum()->supportsMounts() && $station->getMounts()->count( + ) > 0 + ) { foreach ($station->getMounts() as $mount) { if ($showAllMounts || $mount->getIsVisibleOnPublicPages()) { - $mounts[] = $mount->api($fa, $baseUri); + $mounts[] = $mount->api($frontend, $baseUri); } } } $response->mounts = $mounts; $remotes = []; - foreach ($remoteAdapters as $ra_proxy) { - $remote = $ra_proxy->getRemote(); + foreach ($station->getRemotes() as $remote) { if ($showAllMounts || $remote->getIsVisibleOnPublicPages()) { - $remotes[] = $remote->api($ra_proxy->getAdapter()); + $remotes[] = $remote->api( + $this->adapters->getRemoteAdapter($station, $remote) + ); } } + $response->remotes = $remotes; - $response->hls_enabled = $backend->supportsHls() && $station->getEnableHls(); - $response->hls_url = ($response->hls_enabled) + $response->hls_enabled = $station->getBackendTypeEnum()->isEnabled() && $station->getEnableHls(); + $response->hls_url = (null !== $backend && $response->hls_enabled) ? $backend->getHlsUrl($station, $baseUri) : null; diff --git a/src/Entity/Repository/StationRepository.php b/src/Entity/Repository/StationRepository.php index 8dcf4acaf..7ec096cb9 100644 --- a/src/Entity/Repository/StationRepository.php +++ b/src/Entity/Repository/StationRepository.php @@ -9,8 +9,7 @@ use App\Doctrine\ReloadableEntityManagerInterface; use App\Doctrine\Repository; use App\Entity; use App\Flysystem\StationFilesystems; -use App\Radio\Backend\AbstractBackend; -use App\Radio\Frontend\AbstractFrontend; +use App\Radio\Enums\StreamFormats; use App\Service\Flow\UploadedFile; use Azura\Files\ExtendedFilesystemInterface; use Closure; @@ -87,47 +86,49 @@ final class StationRepository extends Repository )->toIterable(); } - /** - * @param string $short_code - */ - public function findByShortCode(string $short_code): ?Entity\Station - { - return $this->repository->findOneBy(['short_name' => $short_code]); - } - /** * Reset mount points to their adapter defaults (in the event of an adapter change). - * - * @param Entity\Station $station - * @param AbstractFrontend $frontend_adapter */ - public function resetMounts(Entity\Station $station, AbstractFrontend $frontend_adapter): void + public function resetMounts(Entity\Station $station): void { foreach ($station->getMounts() as $mount) { $this->em->remove($mount); } // Create default mountpoints if station supports them. - if ($frontend_adapter->supportsMounts()) { - // Create default mount points. - foreach ($frontend_adapter->getDefaultMounts($station) as $mount) { - $this->em->persist($mount); - } + if ($station->getFrontendTypeEnum()->supportsMounts()) { + $record = new Entity\StationMount($station); + $record->setName('/radio.mp3'); + $record->setIsDefault(true); + $record->setEnableAutodj(true); + $record->setAutodjFormat(StreamFormats::Mp3->value); + $record->setAutodjBitrate(128); + $this->em->persist($record); } $this->em->flush(); $this->em->refresh($station); } - public function resetHls(Entity\Station $station, AbstractBackend $backend): void + public function resetHls(Entity\Station $station): void { foreach ($station->getHlsStreams() as $hlsStream) { $this->em->remove($hlsStream); } - if ($station->getEnableHls() && $backend->supportsHls()) { - foreach ($backend->getDefaultHlsStreams($station) as $hlsStream) { - $this->em->persist($hlsStream); + if ($station->getEnableHls() && $station->getBackendTypeEnum()->isEnabled()) { + $streams = [ + 'aac_lofi' => 48, + 'aac_midfi' => 96, + 'aac_hifi' => 192, + ]; + + foreach ($streams as $name => $bitrate) { + $record = new Entity\StationHlsStream($station); + $record->setName($name); + $record->setFormat(StreamFormats::Aac->value); + $record->setBitrate($bitrate); + $this->em->persist($record); } } diff --git a/src/Event/Radio/GenerateRawNowPlaying.php b/src/Event/Radio/GenerateRawNowPlaying.php index d49663249..7ec8bc032 100644 --- a/src/Event/Radio/GenerateRawNowPlaying.php +++ b/src/Event/Radio/GenerateRawNowPlaying.php @@ -5,7 +5,8 @@ declare(strict_types=1); namespace App\Event\Radio; use App\Entity\Station; -use App\Radio; +use App\Radio\Adapters; +use App\Radio\Frontend\AbstractFrontend; use NowPlaying\Result\Result; use Symfony\Contracts\EventDispatcher\Event; @@ -14,9 +15,8 @@ class GenerateRawNowPlaying extends Event protected ?Result $result = null; public function __construct( + protected Adapters $adapters, protected Station $station, - protected Radio\Frontend\AbstractFrontend $frontend, - protected array $remotes, protected bool $include_clients = false ) { } @@ -26,17 +26,21 @@ class GenerateRawNowPlaying extends Event return $this->station; } - public function getFrontend(): Radio\Frontend\AbstractFrontend + public function getFrontend(): ?AbstractFrontend { - return $this->frontend; + return $this->adapters->getFrontendAdapter($this->station); } - /** - * @return Radio\Remote\AdapterProxy[] - */ public function getRemotes(): array { - return $this->remotes; + $remotes = []; + foreach ($this->station->getRemotes() as $remote) { + $remotes[] = [ + $remote, + $this->adapters->getRemoteAdapter($this->station, $remote), + ]; + } + return $remotes; } public function includeClients(): bool diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 3371baf94..e0265e0a0 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -10,7 +10,6 @@ use App\Customization; use App\Entity; use App\Enums\SupportedLocales; use App\Exception; -use App\Radio; use App\RateLimit; use App\Session; use App\View; @@ -30,9 +29,6 @@ final class ServerRequest extends \Slim\Http\ServerRequest public const ATTR_CUSTOMIZATION = 'customization'; public const ATTR_AUTH = 'auth'; public const ATTR_STATION = 'station'; - public const ATTR_STATION_BACKEND = 'station_backend'; - public const ATTR_STATION_FRONTEND = 'station_frontend'; - public const ATTR_STATION_REMOTES = 'station_remotes'; public const ATTR_USER = 'user'; public function getView(): View @@ -95,34 +91,6 @@ final class ServerRequest extends \Slim\Http\ServerRequest return $this->getAttributeOfClass(self::ATTR_STATION, Entity\Station::class); } - public function getStationFrontend(): Radio\Frontend\AbstractFrontend - { - return $this->getAttributeOfClass(self::ATTR_STATION_FRONTEND, Radio\Frontend\AbstractFrontend::class); - } - - public function getStationBackend(): Radio\Backend\AbstractBackend - { - return $this->getAttributeOfClass(self::ATTR_STATION_BACKEND, Radio\Backend\AbstractBackend::class); - } - - /** - * @return Radio\Remote\AdapterProxy[] - * @throws Exception\InvalidRequestAttribute - */ - public function getStationRemotes(): array - { - $remotes = $this->serverRequest->getAttribute(self::ATTR_STATION_REMOTES); - - if (null === $remotes) { - throw new Exception\InvalidRequestAttribute(sprintf( - 'Attribute "%s" was not set.', - self::ATTR_STATION_REMOTES - )); - } - - return $remotes; - } - /** * @param string $attr * @param string $class_name diff --git a/src/Middleware/GetStation.php b/src/Middleware/GetStation.php index 4abd539cc..4f5d22c72 100644 --- a/src/Middleware/GetStation.php +++ b/src/Middleware/GetStation.php @@ -7,7 +7,6 @@ namespace App\Middleware; use App\Entity; use App\Entity\Repository\StationRepository; use App\Http\ServerRequest; -use App\Radio\Adapters; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -20,8 +19,7 @@ use Slim\Routing\RouteContext; class GetStation implements MiddlewareInterface { public function __construct( - protected StationRepository $station_repo, - protected Adapters $adapters + protected StationRepository $station_repo ) { } @@ -35,15 +33,7 @@ class GetStation implements MiddlewareInterface $record = $this->station_repo->findByIdentifier($id); if ($record instanceof Entity\Station) { - $backend = $this->adapters->getBackendAdapter($record); - $frontend = $this->adapters->getFrontendAdapter($record); - $remotes = $this->adapters->getRemoteAdapters($record); - - $request = $request - ->withAttribute(ServerRequest::ATTR_STATION, $record) - ->withAttribute(ServerRequest::ATTR_STATION_BACKEND, $backend) - ->withAttribute(ServerRequest::ATTR_STATION_FRONTEND, $frontend) - ->withAttribute(ServerRequest::ATTR_STATION_REMOTES, $remotes); + $request = $request->withAttribute(ServerRequest::ATTR_STATION, $record); } } diff --git a/src/Middleware/Module/StationFiles.php b/src/Middleware/Module/StationFiles.php index 9ed5a2b6e..981098524 100644 --- a/src/Middleware/Module/StationFiles.php +++ b/src/Middleware/Module/StationFiles.php @@ -16,8 +16,7 @@ class StationFiles { public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { - $backend = $request->getStationBackend(); - if (!$backend->supportsMedia()) { + if (!$request->getStation()->getBackendTypeEnum()->isEnabled()) { throw new Exception(__('This feature is not currently supported on this station.')); } diff --git a/src/Middleware/Module/Stations.php b/src/Middleware/Module/Stations.php index eee883cfc..85df1094a 100644 --- a/src/Middleware/Module/Stations.php +++ b/src/Middleware/Module/Stations.php @@ -29,14 +29,9 @@ class Stations $view = $request->getView(); $station = $request->getStation(); - $backend = $request->getStationBackend(); - $frontend = $request->getStationFrontend(); - $view->addData( [ 'station' => $station, - 'frontend' => $frontend, - 'backend' => $backend, ] ); diff --git a/src/Radio/AbstractAdapter.php b/src/Radio/AbstractLocalAdapter.php similarity index 97% rename from src/Radio/AbstractAdapter.php rename to src/Radio/AbstractLocalAdapter.php index 5763c6d11..69d467641 100644 --- a/src/Radio/AbstractAdapter.php +++ b/src/Radio/AbstractLocalAdapter.php @@ -18,7 +18,7 @@ use Supervisor\Exception\Fault; use Supervisor\Exception\SupervisorException as SupervisorLibException; use Supervisor\SupervisorInterface; -abstract class AbstractAdapter +abstract class AbstractLocalAdapter { public function __construct( protected Environment $environment, @@ -155,14 +155,6 @@ abstract class AbstractAdapter $this->start($station); } - /** - * @return bool Whether this adapter supports a non-destructive reload. - */ - public function supportsReload(): bool - { - return false; - } - /** * Execute a non-destructive reload if the adapter supports it. * diff --git a/src/Radio/Adapters.php b/src/Radio/Adapters.php index 57f8f642d..d22172c77 100644 --- a/src/Radio/Adapters.php +++ b/src/Radio/Adapters.php @@ -6,6 +6,7 @@ namespace App\Radio; use App\Entity; use App\Exception\NotFoundException; +use App\Radio\Backend\Liquidsoap; use App\Radio\Enums\AdapterTypeInterface; use App\Radio\Enums\BackendAdapters; use App\Radio\Enums\FrontendAdapters; @@ -22,19 +23,13 @@ class Adapters ) { } - /** - * @param Entity\Station $station - * - * @throws NotFoundException - */ - public function getFrontendAdapter(Entity\Station $station): Frontend\AbstractFrontend + public function getFrontendAdapter(Entity\Station $station): ?Frontend\AbstractFrontend { - $class_name = $station->getFrontendTypeEnum()->getClass(); - if ($this->adapters->has($class_name)) { - return $this->adapters->get($class_name); - } + $className = $station->getFrontendTypeEnum()->getClass(); - throw new NotFoundException('Adapter not found: ' . $class_name); + return (null !== $className && $this->adapters->has($className)) + ? $this->adapters->get($className) + : null; } /** @@ -46,19 +41,13 @@ class Adapters return $this->listAdaptersFromEnum(FrontendAdapters::cases(), $checkInstalled); } - /** - * @param Entity\Station $station - * - * @throws NotFoundException - */ - public function getBackendAdapter(Entity\Station $station): Backend\AbstractBackend + public function getBackendAdapter(Entity\Station $station): ?Liquidsoap { - $class_name = $station->getBackendTypeEnum()->getClass(); - if ($this->adapters->has($class_name)) { - return $this->adapters->get($class_name); - } + $className = $station->getBackendTypeEnum()->getClass(); - throw new NotFoundException('Adapter not found: ' . $class_name); + return (null !== $className && $this->adapters->has($className)) + ? $this->adapters->get($className) + : null; } /** @@ -70,21 +59,6 @@ class Adapters return $this->listAdaptersFromEnum(BackendAdapters::cases(), $checkInstalled); } - /** - * @param Entity\Station $station - * - * @return Remote\AdapterProxy[] - * @throws NotFoundException - */ - public function getRemoteAdapters(Entity\Station $station): array - { - $remote_adapters = []; - foreach ($station->getRemotes() as $remote) { - $remote_adapters[] = new Remote\AdapterProxy($this->getRemoteAdapter($station, $remote), $remote); - } - return $remote_adapters; - } - public function getRemoteAdapter(Entity\Station $station, Entity\StationRemote $remote): Remote\AbstractRemote { $class_name = $remote->getTypeEnum()->getClass(); @@ -113,8 +87,8 @@ class Adapters $adapters = []; foreach ($cases as $adapter) { $adapters[$adapter->getValue()] = [ - 'enum' => $adapter, - 'name' => $adapter->getName(), + 'enum' => $adapter, + 'name' => $adapter->getName(), 'class' => $adapter->getClass(), ]; } @@ -123,7 +97,11 @@ class Adapters return array_filter( $adapters, function ($adapter_info) { - /** @var AbstractAdapter $adapter */ + if (null === $adapter_info['class']) { + return true; + } + + /** @var AbstractLocalAdapter $adapter */ $adapter = $this->adapters->get($adapter_info['class']); return $adapter->isInstalled(); } diff --git a/src/Radio/AutoDJ/Annotations.php b/src/Radio/AutoDJ/Annotations.php index 43f9d854c..21ad9819a 100644 --- a/src/Radio/AutoDJ/Annotations.php +++ b/src/Radio/AutoDJ/Annotations.php @@ -69,7 +69,9 @@ class Annotations implements EventSubscriberInterface $event->setSongPath('media:' . ltrim($media->getPath(), '/')); $backend = $this->adapters->getBackendAdapter($event->getStation()); - $event->addAnnotations($backend->annotateMedia($media)); + if (null !== $backend) { + $event->addAnnotations($backend->annotateMedia($media)); + } } else { $queue = $event->getQueue(); if ($queue instanceof Entity\StationQueue) { diff --git a/src/Radio/Backend/AbstractBackend.php b/src/Radio/Backend/AbstractBackend.php deleted file mode 100644 index 83d2aeecb..000000000 --- a/src/Radio/Backend/AbstractBackend.php +++ /dev/null @@ -1,93 +0,0 @@ -setName($name); - $record->setFormat(StreamFormats::Aac->value); - $record->setBitrate($bitrate); - return $record; - }, - ['aac_lofi', 'aac_midfi', 'aac_hifi'], - [48, 96, 192] - ); - } - - public function getHlsUrl(Entity\Station $station, UriInterface $baseUrl = null): UriInterface - { - if (!$this->supportsHls()) { - throw new RuntimeException('Cannot generate HLS URL.'); - } - - $baseUrl ??= $this->router->getBaseUrl(); - - return $baseUrl->withPath( - $baseUrl->getPath() . CustomUrls::getHlsUrl($station) . '/live.m3u8' - ); - } - - public function getStreamPort(Entity\Station $station): ?int - { - return null; - } - - /** - * @param Entity\StationMedia $media - * - * @return mixed[] - */ - public function annotateMedia(Entity\StationMedia $media): array - { - return []; - } - - public function getProgramName(Entity\Station $station): string - { - return 'station_' . $station->getId() . ':station_' . $station->getId() . '_backend'; - } -} diff --git a/src/Radio/Backend/Liquidsoap.php b/src/Radio/Backend/Liquidsoap.php index 43cbf9201..b386bc5c8 100644 --- a/src/Radio/Backend/Liquidsoap.php +++ b/src/Radio/Backend/Liquidsoap.php @@ -8,43 +8,14 @@ use App\Entity; use App\Event\Radio\WriteLiquidsoapConfiguration; use App\Exception; use App\Nginx\CustomUrls; +use App\Radio\AbstractLocalAdapter; use App\Radio\Enums\LiquidsoapQueues; use LogicException; use Psr\Http\Message\UriInterface; use Symfony\Component\Process\Process; -class Liquidsoap extends AbstractBackend +class Liquidsoap extends AbstractLocalAdapter { - public function supportsMedia(): bool - { - return true; - } - - public function supportsRequests(): bool - { - return true; - } - - public function supportsStreamers(): bool - { - return true; - } - - public function supportsWebStreaming(): bool - { - return true; - } - - public function supportsImmediateQueue(): bool - { - return true; - } - - public function supportsHls(): bool - { - return true; - } - /** * @inheritDoc */ @@ -234,6 +205,14 @@ class Liquidsoap extends AbstractBackend : null; } + public function getHlsUrl(Entity\Station $station, UriInterface $baseUrl = null): UriInterface + { + $baseUrl ??= $this->router->getBaseUrl(); + return $baseUrl->withPath( + $baseUrl->getPath() . CustomUrls::getHlsUrl($station) . '/live.m3u8' + ); + } + public function isQueueEmpty( Entity\Station $station, LiquidsoapQueues $queue @@ -337,4 +316,9 @@ class Liquidsoap extends AbstractBackend throw new LogicException($process->getOutput()); } } + + public function getProgramName(Entity\Station $station): string + { + return 'station_' . $station->getIdRequired() . ':station_' . $station->getIdRequired() . '_backend'; + } } diff --git a/src/Radio/Backend/None.php b/src/Radio/Backend/None.php deleted file mode 100644 index f12f0dcfe..000000000 --- a/src/Radio/Backend/None.php +++ /dev/null @@ -1,23 +0,0 @@ -logger->error( - 'Cannot start process; AutoDJ is currently disabled.', - ['station_id' => $station->getId(), 'station_name' => $station->getName()] - ); - } -} diff --git a/src/Radio/Configuration.php b/src/Radio/Configuration.php index fc5cd5d20..a15f64c5d 100644 --- a/src/Radio/Configuration.php +++ b/src/Radio/Configuration.php @@ -113,51 +113,52 @@ class Configuration throw new RuntimeException('Station is disabled.'); } + $frontendEnum = $station->getFrontendTypeEnum(); + $backendEnum = $station->getBackendTypeEnum(); + $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)) { + if ( + (null === $frontend || !$frontend->hasCommand($station)) + && (null === $backend || !$backend->hasCommand($station)) + ) { $this->unlinkAndStopStation($station, $reloadSupervisor); throw new RuntimeException('Station has no local services.'); } // If using AutoDJ and there is no media, don't spin up services. if ( - BackendAdapters::None !== $station->getBackendTypeEnum() + $backendEnum->isEnabled() && !$this->stationPlaylistRepo->stationHasActivePlaylists($station) ) { $this->unlinkAndStopStation($station, $reloadSupervisor); throw new RuntimeException('Station has no media assigned to playlists.'); } - // Get group information - $backend_name = $backend->getProgramName($station); - [$backend_group, $backend_program] = explode(':', $backend_name); - - $frontend_name = $frontend->getProgramName($station); - [, $frontend_program] = explode(':', $frontend_name); - // Write group section of config $programs = []; - if ($backend->hasCommand($station)) { - $programs[] = $backend_program; + if (null !== $backend && $backend->hasCommand($station)) { + $programs[] = (explode(':', $backend->getProgramName($station)))[2]; } - if ($frontend->hasCommand($station)) { - $programs[] = $frontend_program; + if (null !== $frontend && $frontend->hasCommand($station)) { + $programs[] = (explode(':', $frontend->getProgramName($station)))[2]; } - $supervisorConfig[] = '[group:' . $backend_group . ']'; + $stationGroup = 'station_' . $station->getIdRequired(); + + $supervisorConfig[] = '[group:' . $stationGroup . ']'; $supervisorConfig[] = 'programs=' . implode(',', $programs); $supervisorConfig[] = ''; // Write frontend - if ($frontend->hasCommand($station)) { + if (null !== $frontend && $frontend->hasCommand($station)) { $supervisorConfig[] = $this->writeConfigurationSection($station, $frontend, 90); } // Write backend - if ($backend->hasCommand($station)) { + if (null !== $backend && $backend->hasCommand($station)) { $supervisorConfig[] = $this->writeConfigurationSection($station, $backend, 100); } @@ -166,22 +167,22 @@ class Configuration file_put_contents($supervisorConfigFile, $supervisor_config_data); // Write supporting configurations. - $frontend->write($station); - $backend->write($station); + $frontend?->write($station); + $backend?->write($station); // Reload Supervisord and process groups if ($reloadSupervisor) { $affected_groups = $this->reloadSupervisor(); - $was_restarted = in_array($backend_group, $affected_groups, true); + $was_restarted = in_array($stationGroup, $affected_groups, true); if (!$was_restarted && $forceRestart) { try { - if ($attemptReload && ($backend->supportsReload() || $frontend->supportsReload())) { - $backend->reload($station); - $frontend->reload($station); + if ($attemptReload && ($backendEnum->isEnabled() || $frontendEnum->supportsReload())) { + $backend?->reload($station); + $frontend?->reload($station); } else { - $this->supervisor->stopProcessGroup($backend_group); - $this->supervisor->startProcessGroup($backend_group); + $this->supervisor->stopProcessGroup($stationGroup); + $this->supervisor->startProcessGroup($stationGroup); } } catch (SupervisorException) { } @@ -310,8 +311,8 @@ class Configuration public function assignRadioPorts(Station $station, bool $force = false): void { if ( - FrontendAdapters::Remote !== $station->getFrontendTypeEnum() - || BackendAdapters::Liquidsoap !== $station->getBackendTypeEnum() + $station->getFrontendTypeEnum()->isEnabled() + || $station->getBackendTypeEnum()->isEnabled() ) { $frontend_config = $station->getFrontendConfig(); $backend_config = $station->getBackendConfig(); @@ -444,7 +445,7 @@ class Configuration protected function writeConfigurationSection( Station $station, - AbstractAdapter $adapter, + AbstractLocalAdapter $adapter, ?int $priority ): string { [, $program_name] = explode(':', $adapter->getProgramName($station)); diff --git a/src/Radio/Enums/AdapterTypeInterface.php b/src/Radio/Enums/AdapterTypeInterface.php index 2869e277a..b4d8d7594 100644 --- a/src/Radio/Enums/AdapterTypeInterface.php +++ b/src/Radio/Enums/AdapterTypeInterface.php @@ -10,5 +10,5 @@ interface AdapterTypeInterface public function getName(): string; - public function getClass(): string; + public function getClass(): ?string; } diff --git a/src/Radio/Enums/BackendAdapters.php b/src/Radio/Enums/BackendAdapters.php index 1bfd8c180..16479cb2d 100644 --- a/src/Radio/Enums/BackendAdapters.php +++ b/src/Radio/Enums/BackendAdapters.php @@ -7,7 +7,6 @@ declare(strict_types=1); namespace App\Radio\Enums; use App\Radio\Backend\Liquidsoap; -use App\Radio\Backend\None; enum BackendAdapters: string implements AdapterTypeInterface { @@ -27,14 +26,19 @@ enum BackendAdapters: string implements AdapterTypeInterface }; } - public function getClass(): string + public function getClass(): ?string { return match ($this) { self::Liquidsoap => Liquidsoap::class, - self::None => None::class, + default => null, }; } + public function isEnabled(): bool + { + return self::None !== $this; + } + public static function default(): self { return self::Liquidsoap; diff --git a/src/Radio/Enums/FrontendAdapters.php b/src/Radio/Enums/FrontendAdapters.php index 921ab1a19..1433d5cbc 100644 --- a/src/Radio/Enums/FrontendAdapters.php +++ b/src/Radio/Enums/FrontendAdapters.php @@ -7,7 +7,6 @@ declare(strict_types=1); namespace App\Radio\Enums; use App\Radio\Frontend\Icecast; -use App\Radio\Frontend\Remote; use App\Radio\Frontend\Shoutcast; enum FrontendAdapters: string implements AdapterTypeInterface @@ -30,15 +29,33 @@ enum FrontendAdapters: string implements AdapterTypeInterface }; } - public function getClass(): string + public function getClass(): ?string { return match ($this) { self::Icecast => Icecast::class, self::Shoutcast => Shoutcast::class, - self::Remote => Remote::class, + default => null }; } + public function isEnabled(): bool + { + return self::Remote !== $this; + } + + public function supportsMounts(): bool + { + return match ($this) { + self::Shoutcast, self::Icecast => true, + default => false + }; + } + + public function supportsReload(): bool + { + return self::Icecast === $this; + } + public static function default(): self { return self::Icecast; diff --git a/src/Radio/Frontend/AbstractFrontend.php b/src/Radio/Frontend/AbstractFrontend.php index bdcc17285..844842c77 100644 --- a/src/Radio/Frontend/AbstractFrontend.php +++ b/src/Radio/Frontend/AbstractFrontend.php @@ -8,8 +8,7 @@ use App\Entity; use App\Environment; use App\Http\Router; use App\Nginx\CustomUrls; -use App\Radio\AbstractAdapter; -use App\Radio\Enums\StreamFormats; +use App\Radio\AbstractLocalAdapter; use App\Xml\Reader; use Doctrine\ORM\EntityManagerInterface; use Exception; @@ -24,7 +23,7 @@ use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; use Supervisor\SupervisorInterface; -abstract class AbstractFrontend extends AbstractAdapter +abstract class AbstractFrontend extends AbstractLocalAdapter { public function __construct( Environment $environment, @@ -41,31 +40,6 @@ abstract class AbstractFrontend extends AbstractAdapter parent::__construct($environment, $em, $supervisor, $dispatcher, $logger, $router); } - /** - * @return bool Whether the station supports multiple mount points per station - */ - public function supportsMounts(): bool - { - return false; - } - - /** - * Get the default mounts when resetting or initializing a station. - * - * @return Entity\StationMount[] - */ - public function getDefaultMounts(Entity\Station $station): array - { - $record = new Entity\StationMount($station); - $record->setName('/radio.mp3'); - $record->setIsDefault(true); - $record->setEnableAutodj(true); - $record->setAutodjFormat(StreamFormats::Mp3->value); - $record->setAutodjBitrate(128); - - return [$record]; - } - /** * @inheritdoc */ diff --git a/src/Radio/Frontend/Icecast.php b/src/Radio/Frontend/Icecast.php index b06aa05f5..8dcbad569 100644 --- a/src/Radio/Frontend/Icecast.php +++ b/src/Radio/Frontend/Icecast.php @@ -22,16 +22,6 @@ class Icecast extends AbstractFrontend public const LOGLEVEL_WARN = 2; public const LOGLEVEL_ERROR = 1; - public function supportsMounts(): bool - { - return true; - } - - public function supportsReload(): bool - { - return true; - } - public function reload(Entity\Station $station): void { if ($this->hasCommand($station)) { diff --git a/src/Radio/Frontend/Remote.php b/src/Radio/Frontend/Remote.php deleted file mode 100644 index 5df4cff9d..000000000 --- a/src/Radio/Frontend/Remote.php +++ /dev/null @@ -1,35 +0,0 @@ -adapter; - } - - public function getRemote(): Entity\StationRemote - { - return $this->remote; - } -} diff --git a/src/Service/Acme.php b/src/Service/Acme.php index f1241d9b4..1e67f9d92 100644 --- a/src/Service/Acme.php +++ b/src/Service/Acme.php @@ -176,13 +176,13 @@ final class Acme $this->nginx->reload(); foreach ($this->stationRepo->iterateEnabledStations() as $station) { - if (!$station->getHasStarted()) { + $frontendType = $station->getFrontendTypeEnum(); + if (!$station->getHasStarted() || !$frontendType->supportsReload()) { continue; } $frontend = $this->adapters->getFrontendAdapter($station); - - if ($frontend->supportsReload() && $frontend->isRunning($station)) { + if (null !== $frontend && $frontend->isRunning($station)) { $frontend->reload($station); } } diff --git a/src/Sync/NowPlaying/Task/NowPlayingTask.php b/src/Sync/NowPlaying/Task/NowPlayingTask.php index 91df3df86..a0c3ab85f 100644 --- a/src/Sync/NowPlaying/Task/NowPlayingTask.php +++ b/src/Sync/NowPlaying/Task/NowPlayingTask.php @@ -63,15 +63,11 @@ class NowPlayingTask implements NowPlayingTaskInterface, EventSubscriberInterfac $include_clients = $this->settingsRepo->readSettings()->isAnalyticsEnabled(); - $frontend_adapter = $this->adapters->getFrontendAdapter($station); - $remote_adapters = $this->adapters->getRemoteAdapters($station); - // Build the new "raw" NowPlaying data. try { $event = new GenerateRawNowPlaying( + $this->adapters, $station, - $frontend_adapter, - $remote_adapters, $include_clients ); $this->eventDispatcher->dispatch($event); @@ -121,15 +117,13 @@ class NowPlayingTask implements NowPlayingTaskInterface, EventSubscriberInterfac public function loadRawFromFrontend(GenerateRawNowPlaying $event): void { try { - $result = $event - ->getFrontend() - ->getNowPlaying($event->getStation(), $event->includeClients()); + $result = $event->getFrontend()?->getNowPlaying($event->getStation(), $event->includeClients()); + if (null !== $result) { + $event->setResult($result); + } } catch (Exception $e) { $this->logger->error(sprintf('NowPlaying adapter error: %s', $e->getMessage())); - return; } - - $event->setResult($result); } public function addToRawFromRemotes(GenerateRawNowPlaying $event): void @@ -137,11 +131,11 @@ class NowPlayingTask implements NowPlayingTaskInterface, EventSubscriberInterfac $result = $event->getResult(); // Loop through all remotes and update NP data accordingly. - foreach ($event->getRemotes() as $ra_proxy) { + foreach ($event->getRemotes() as [$remote, $adapter]) { try { - $result = $ra_proxy->getAdapter()->updateNowPlaying( + $result = $adapter->updateNowPlaying( $result, - $ra_proxy->getRemote(), + $remote, $event->includeClients() ); } catch (Exception $e) {