From 4371ac3be387183f464fbb2d732e1d4f4108afcd Mon Sep 17 00:00:00 2001 From: Bjarn Bronsveld <14638441+bjarn@users.noreply.github.com> Date: Mon, 23 May 2022 06:50:55 +0200 Subject: [PATCH] feat: add stereo tool support for stations (#5344) Co-authored-by: Buster "Silver Eagle" Neece Co-authored-by: Vaalyn --- CHANGELOG.md | 7 +- config/menus/admin.php | 23 ++- config/menus/station.php | 12 ++ config/routes/admin.php | 3 + config/routes/api_admin.php | 10 + config/routes/api_station.php | 20 ++ config/routes/stations.php | 4 + docker-compose.sample.yml | 2 + .../Admin/Stations/Form/BackendForm.vue | 191 +++++++++++++++--- .../components/Admin/Stations/StationForm.vue | 29 ++- frontend/vue/components/Admin/StereoTool.vue | 103 ++++++++++ .../vue/components/Entity/RadioAdapters.js | 4 + .../components/Stations/StereoToolConfig.vue | 101 +++++++++ frontend/vue/pages/Admin/StereoTool.js | 7 + .../vue/pages/Stations/StereoToolConfig.js | 7 + frontend/webpack.config.js | 2 + src/Controller/Admin/StereoToolAction.php | 29 +++ .../Api/Admin/StereoTool/GetAction.php | 30 +++ .../Api/Admin/StereoTool/PostAction.php | 41 ++++ .../DeleteStereoToolConfigurationAction.php | 47 +++++ .../GetStereoToolConfigurationAction.php | 61 ++++++ .../PostStereoToolConfigurationAction.php | 53 +++++ src/Controller/Stations/ProfileController.php | 6 +- .../Stations/UploadStereoToolConfigAction.php | 33 +++ src/Entity/Repository/StationRepository.php | 51 +++++ src/Entity/StationBackendConfiguration.php | 49 ++++- src/OpenApi.php | 1 + src/Radio/Backend/Liquidsoap/ConfigWriter.php | 31 ++- src/Radio/Enums/AudioProcessingMethods.php | 28 +++ src/Radio/StereoTool.php | 57 ++++++ src/VueComponent/StationFormComponent.php | 3 + .../roles/azuracast-config/tasks/main.yml | 1 + util/docker/common/add_user.sh | 2 +- util/docker/mariadb/setup/mariadb.sh | 3 +- 34 files changed, 993 insertions(+), 58 deletions(-) create mode 100644 frontend/vue/components/Admin/StereoTool.vue create mode 100644 frontend/vue/components/Stations/StereoToolConfig.vue create mode 100644 frontend/vue/pages/Admin/StereoTool.js create mode 100644 frontend/vue/pages/Stations/StereoToolConfig.js create mode 100644 src/Controller/Admin/StereoToolAction.php create mode 100644 src/Controller/Api/Admin/StereoTool/GetAction.php create mode 100644 src/Controller/Api/Admin/StereoTool/PostAction.php create mode 100644 src/Controller/Api/Stations/StereoTool/DeleteStereoToolConfigurationAction.php create mode 100644 src/Controller/Api/Stations/StereoTool/GetStereoToolConfigurationAction.php create mode 100644 src/Controller/Api/Stations/StereoTool/PostStereoToolConfigurationAction.php create mode 100644 src/Controller/Stations/UploadStereoToolConfigAction.php create mode 100644 src/Radio/Enums/AudioProcessingMethods.php create mode 100644 src/Radio/StereoTool.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d4fac4e3..a80dad2ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ release channel, you can take advantage of these new features and fixes. ## New Features/Changes +- **Integrated Stereo Tool Support**: We now support the popular premium sound processing tool, Stereo Tool. Because the + software is proprietary, you must first upload a copy of it via the System Administration page; you can then configure + Stereo Tool on a per-station level, including uploading your own custom `.sts` configuration file. + - **Bulk Media CSV Import/Export**: You can now export all of your station's media and its associated metadata into a CSV file for editing in spreadsheet software of your choice. Once you've made your changes, upload the modified file from the same page and all of the changes will be applied in bulk, including basic metadata, associated playlists, @@ -24,7 +28,8 @@ release channel, you can take advantage of these new features and fixes. once again be possible. - Docker users can now debug Slim Application Errors by editing the `SHOW_DETAILED_ERRORS` in the `azuracast.env` file, - reports should be submitted to our [issues](https://github.com/azuracast/azuracast/issues) section for review by our team. + reports should be submitted to our [issues](https://github.com/azuracast/azuracast/issues) section for review by our + team. ## Bug Fixes diff --git a/config/menus/admin.php b/config/menus/admin.php index 8c86b2c6f..19e679c94 100644 --- a/config/menus/admin.php +++ b/config/menus/admin.php @@ -87,19 +87,24 @@ return function (App\Event\BuildAdminMenu $e) { 'url' => (string)$router->named('admin:custom_fields:index'), 'permission' => GlobalPermissions::CustomFields, ], - 'relays' => [ - 'label' => __('Connected AzuraRelays'), - 'url' => (string)$router->named('admin:relays:index'), + 'relays' => [ + 'label' => __('Connected AzuraRelays'), + 'url' => (string)$router->named('admin:relays:index'), 'permission' => GlobalPermissions::Stations, ], - 'shoutcast' => [ - 'label' => __('Install SHOUTcast'), - 'url' => (string)$router->named('admin:install_shoutcast:index'), + 'shoutcast' => [ + 'label' => __('Install SHOUTcast'), + 'url' => (string)$router->named('admin:install_shoutcast:index'), 'permission' => GlobalPermissions::All, ], - 'geolite' => [ - 'label' => __('Install GeoLite IP Database'), - 'url' => (string)$router->named('admin:install_geolite:index'), + 'stereo_tool' => [ + 'label' => __('Install Stereo Tool'), + 'url' => (string)$router->named('admin:install_stereo_tool:index'), + 'permission' => GlobalPermissions::All, + ], + 'geolite' => [ + 'label' => __('Install GeoLite IP Database'), + 'url' => (string)$router->named('admin:install_geolite:index'), 'permission' => GlobalPermissions::All, ], ], diff --git a/config/menus/station.php b/config/menus/station.php index db6ef1f9d..da0261172 100644 --- a/config/menus/station.php +++ b/config/menus/station.php @@ -4,11 +4,14 @@ */ use App\Enums\StationPermissions; +use App\Radio\Enums\AudioProcessingMethods; return function (App\Event\BuildStationMenu $e) { $request = $e->getRequest(); $station = $e->getStation(); + $backendConfig = $station->getBackendConfig(); + $router = $request->getRouter(); $backend = $request->getStationBackend(); $frontend = $request->getStationFrontend(); @@ -240,6 +243,15 @@ return function (App\Event\BuildStationMenu $e) { && $backend instanceof App\Radio\Backend\Liquidsoap, 'permission' => StationPermissions::Broadcasting, ], + 'stations:stereo_tool_config' => [ + 'label' => __('Upload Stereo Tool Configuration'), + 'class' => 'text-muted', + 'url' => (string)$router->fromHere('stations:stereo_tool_config'), + 'visible' => $settings->getEnableAdvancedFeatures() + && $backend instanceof App\Radio\Backend\Liquidsoap + && AudioProcessingMethods::StereoTool === $backendConfig->getAudioProcessingMethodEnum(), + 'permission' => StationPermissions::Broadcasting, + ], 'queue' => [ 'label' => __('Upcoming Song Queue'), 'class' => 'text-muted', diff --git a/config/routes/admin.php b/config/routes/admin.php index 18271d775..9cc25847d 100644 --- a/config/routes/admin.php +++ b/config/routes/admin.php @@ -63,6 +63,9 @@ return static function (RouteCollectorProxy $app) { $group->get('/shoutcast', Controller\Admin\ShoutcastAction::class) ->setName('admin:install_shoutcast:index'); + $group->get('/stereo_tool', Controller\Admin\StereoToolAction::class) + ->setName('admin:install_stereo_tool:index'); + $group->get('/geolite', Controller\Admin\GeoLiteAction::class) ->setName('admin:install_geolite:index'); } diff --git a/config/routes/api_admin.php b/config/routes/api_admin.php index efec80004..ca14a6909 100644 --- a/config/routes/api_admin.php +++ b/config/routes/api_admin.php @@ -122,6 +122,16 @@ return static function (RouteCollectorProxy $group) { '/shoutcast', Controller\Api\Admin\Shoutcast\PostAction::class ); + + $group->get( + '/stereo_tool', + Controller\Api\Admin\StereoTool\GetAction::class + )->setName('api:admin:stereo_tool'); + + $group->post( + '/stereo_tool', + Controller\Api\Admin\StereoTool\PostAction::class + ); } )->add(new Middleware\Permissions(GlobalPermissions::Settings)); diff --git a/config/routes/api_station.php b/config/routes/api_station.php index d97b50f3d..b6a40f187 100644 --- a/config/routes/api_station.php +++ b/config/routes/api_station.php @@ -137,6 +137,26 @@ return static function (RouteCollectorProxy $group) { } )->add(new Middleware\Permissions(StationPermissions::Broadcasting, true)); + $group->group( + '/stereo_tool_config', + function (RouteCollectorProxy $group) { + $group->get( + '', + Controller\Api\Stations\StereoTool\GetStereoToolConfigurationAction::class + )->setName('api:stations:stereo_tool_config'); + + $group->post( + '', + Controller\Api\Stations\StereoTool\PostStereoToolConfigurationAction::class + ); + + $group->delete( + '', + Controller\Api\Stations\StereoTool\DeleteStereoToolConfigurationAction::class + ); + } + )->add(new Middleware\Permissions(StationPermissions::Broadcasting, true)); + // Public and private podcast pages $group->group( '/podcast/{podcast_id}', diff --git a/config/routes/stations.php b/config/routes/stations.php index f6ab39444..fd6f954f5 100644 --- a/config/routes/stations.php +++ b/config/routes/stations.php @@ -42,6 +42,10 @@ return static function (RouteCollectorProxy $app) { ->setName('stations:util:ls_config') ->add(new Middleware\Permissions(StationPermissions::Broadcasting, true)); + $group->get('/stereo_tool_config', Controller\Stations\UploadStereoToolConfigAction::class) + ->setName('stations:stereo_tool_config') + ->add(new Middleware\Permissions(StationPermissions::Broadcasting, true)); + $group->group( '/logs', function (RouteCollectorProxy $group) { diff --git a/docker-compose.sample.yml b/docker-compose.sample.yml index d941ad48e..f37f21527 100644 --- a/docker-compose.sample.yml +++ b/docker-compose.sample.yml @@ -188,6 +188,7 @@ services: - www_uploads:/var/azuracast/uploads - station_data:/var/azuracast/stations - shoutcast2_install:/var/azuracast/servers/shoutcast2 + - stereo_tool_install:/var/azuracast/servers/stereo_tool - geolite_install:/var/azuracast/geoip - sftpgo_data:/var/azuracast/sftpgo/persist - backups:/var/azuracast/backups @@ -207,6 +208,7 @@ volumes: letsencrypt: { } letsencrypt_acme: { } shoutcast2_install: { } + stereo_tool_install: { } geolite_install: { } sftpgo_data: { } station_data: { } diff --git a/frontend/vue/components/Admin/Stations/Form/BackendForm.vue b/frontend/vue/components/Admin/Stations/Form/BackendForm.vue index e0eca4652..a63ea4ed6 100644 --- a/frontend/vue/components/Admin/Stations/Form/BackendForm.vue +++ b/frontend/vue/components/Admin/Stations/Form/BackendForm.vue @@ -8,7 +8,9 @@ AutoDJ Service - - + + + - + + + + + + + + + + + + + + + + +

+ Upload a Stereo Tool configuration file from the "Broadcasting" submenu in the station profile. +

+
+
+
+
+ @@ -77,7 +127,9 @@ Allow Song Requests @@ -92,7 +144,10 @@ Request Minimum Delay (Minutes) @@ -103,7 +158,10 @@ Request Last Played Threshold (Minutes) @@ -123,7 +181,9 @@ Allow Streamers / DJs @@ -138,7 +198,9 @@ Record Live Broadcasts @@ -183,20 +245,28 @@ Deactivate Streamer on Disconnect (Seconds) - @@ -207,7 +277,10 @@ DJ/Streamer Buffer Time (Seconds) @@ -218,7 +291,9 @@ Customize DJ/Streamer Mount Point @@ -239,7 +314,10 @@ Manual AutoDJ Mode @@ -250,7 +328,9 @@ Use Replaygain Metadata @@ -261,7 +341,9 @@ Customize Internal Request Processing Port @@ -272,7 +354,9 @@ AutoDJ Queue Length @@ -282,7 +366,10 @@ Character Set Encoding - - diff --git a/frontend/vue/components/Entity/RadioAdapters.js b/frontend/vue/components/Entity/RadioAdapters.js index 82e7f9c9d..0013be54e 100644 --- a/frontend/vue/components/Entity/RadioAdapters.js +++ b/frontend/vue/components/Entity/RadioAdapters.js @@ -5,6 +5,10 @@ export const FRONTEND_REMOTE = 'remote'; export const BACKEND_LIQUIDSOAP = 'liquidsoap'; export const BACKEND_NONE = 'none'; +export const AUDIO_PROCESSING_LIQUIDSOAP = 'nrj'; +export const AUDIO_PROCESSING_STEREO_TOOL = 'stereo_tool'; +export const AUDIO_PROCESSING_NONE = 'none'; + export const REMOTE_SHOUTCAST1 = 'shoutcast1'; export const REMOTE_SHOUTCAST2 = 'shoutcast2'; export const REMOTE_ICECAST = 'icecast'; diff --git a/frontend/vue/components/Stations/StereoToolConfig.vue b/frontend/vue/components/Stations/StereoToolConfig.vue new file mode 100644 index 000000000..994aa54f3 --- /dev/null +++ b/frontend/vue/components/Stations/StereoToolConfig.vue @@ -0,0 +1,101 @@ + + + diff --git a/frontend/vue/pages/Admin/StereoTool.js b/frontend/vue/pages/Admin/StereoTool.js new file mode 100644 index 000000000..47c8fbef8 --- /dev/null +++ b/frontend/vue/pages/Admin/StereoTool.js @@ -0,0 +1,7 @@ +import initBase from '~/base.js'; + +import '~/vendor/bootstrapVue.js'; + +import AdminStereoTool from '~/components/Admin/StereoTool.vue'; + +export default initBase(AdminStereoTool); diff --git a/frontend/vue/pages/Stations/StereoToolConfig.js b/frontend/vue/pages/Stations/StereoToolConfig.js new file mode 100644 index 000000000..139c86260 --- /dev/null +++ b/frontend/vue/pages/Stations/StereoToolConfig.js @@ -0,0 +1,7 @@ +import initBase from '~/base.js'; + +import '~/vendor/bootstrapVue.js'; + +import StereoToolConfig from '~/components/Stations/StereoToolConfig.vue'; + +export default initBase(StereoToolConfig); diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 7a3ddb8d3..19c62179a 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -18,6 +18,7 @@ module.exports = { AdminPermissions: '~/pages/Admin/Permissions.js', AdminSettings: '~/pages/Admin/Settings.js', AdminShoutcast: '~/pages/Admin/Shoutcast.js', + AdminStereoTool: '~/pages/Admin/StereoTool.js', AdminStations: '~/pages/Admin/Stations.js', AdminStorageLocations: '~/pages/Admin/StorageLocations.js', AdminUsers: '~/pages/Admin/Users.js', @@ -44,6 +45,7 @@ module.exports = { StationsProfileEdit: '~/pages/Stations/ProfileEdit.js', StationsQueue: '~/pages/Stations/Queue.js', StationsRemotes: '~/pages/Stations/Remotes.js', + StationsStereoToolConfig: '~/pages/Stations/StereoToolConfig.js', StationsStreamers: '~/pages/Stations/Streamers.js', StationsReportsListeners: '~/pages/Stations/Reports/Listeners.js', StationsReportsRequests: '~/pages/Stations/Reports/Requests.js', diff --git a/src/Controller/Admin/StereoToolAction.php b/src/Controller/Admin/StereoToolAction.php new file mode 100644 index 000000000..35692e7fe --- /dev/null +++ b/src/Controller/Admin/StereoToolAction.php @@ -0,0 +1,29 @@ +getRouter(); + + return $request->getView()->renderVuePage( + response: $response, + component: 'Vue_AdminStereoTool', + id: 'admin-stereo-tool', + title: __('Install Stereo Tool'), + props: [ + 'apiUrl' => (string)$router->named('api:admin:stereo_tool'), + ], + ); + } +} diff --git a/src/Controller/Api/Admin/StereoTool/GetAction.php b/src/Controller/Api/Admin/StereoTool/GetAction.php new file mode 100644 index 000000000..09cc6cdf2 --- /dev/null +++ b/src/Controller/Api/Admin/StereoTool/GetAction.php @@ -0,0 +1,30 @@ +withJson( + [ + 'success' => true, + 'version' => $this->stereoTool->getVersion(), + ] + ); + } +} diff --git a/src/Controller/Api/Admin/StereoTool/PostAction.php b/src/Controller/Api/Admin/StereoTool/PostAction.php new file mode 100644 index 000000000..ef4c51cb2 --- /dev/null +++ b/src/Controller/Api/Admin/StereoTool/PostAction.php @@ -0,0 +1,41 @@ +stereoTool->getBinaryPath(); + if (is_file($binaryPath)) { + unlink($binaryPath); + } + + $flowResponse->moveTo($binaryPath); + + chmod($binaryPath, 0744); + + return $response->withJson(Entity\Api\Status::success()); + } +} diff --git a/src/Controller/Api/Stations/StereoTool/DeleteStereoToolConfigurationAction.php b/src/Controller/Api/Stations/StereoTool/DeleteStereoToolConfigurationAction.php new file mode 100644 index 000000000..910f93654 --- /dev/null +++ b/src/Controller/Api/Stations/StereoTool/DeleteStereoToolConfigurationAction.php @@ -0,0 +1,47 @@ +getStation(); + + $this->stationRepo->clearStereoToolConfiguration($station); + + return $response->withJson(Entity\Api\Status::deleted()); + } +} diff --git a/src/Controller/Api/Stations/StereoTool/GetStereoToolConfigurationAction.php b/src/Controller/Api/Stations/StereoTool/GetStereoToolConfigurationAction.php new file mode 100644 index 000000000..5204e863d --- /dev/null +++ b/src/Controller/Api/Stations/StereoTool/GetStereoToolConfigurationAction.php @@ -0,0 +1,61 @@ +getStation(); + + $stereoToolConfigurationPath = $station->getBackendConfig()->getStereoToolConfigurationPath(); + + if (!empty($stereoToolConfigurationPath)) { + $fsConfig = (new StationFilesystems($station))->getConfigFilesystem(); + + if ($fsConfig->fileExists($stereoToolConfigurationPath)) { + return $response->streamFilesystemFile( + $fsConfig, + $stereoToolConfigurationPath, + basename($stereoToolConfigurationPath) + ); + } + } + + return $response->withStatus(404) + ->withJson(Entity\Api\Error::notFound()); + } +} diff --git a/src/Controller/Api/Stations/StereoTool/PostStereoToolConfigurationAction.php b/src/Controller/Api/Stations/StereoTool/PostStereoToolConfigurationAction.php new file mode 100644 index 000000000..a1b9bf07b --- /dev/null +++ b/src/Controller/Api/Stations/StereoTool/PostStereoToolConfigurationAction.php @@ -0,0 +1,53 @@ +getStation(); + + $flowResponse = Flow::process($request, $response, $station->getRadioTempDir()); + if ($flowResponse instanceof ResponseInterface) { + return $flowResponse; + } + + $this->stationRepo->setStereoToolConfiguration($station, $flowResponse); + + return $response->withJson(Entity\Api\Status::updated()); + } +} diff --git a/src/Controller/Stations/ProfileController.php b/src/Controller/Stations/ProfileController.php index 45413c1f6..da4511345 100644 --- a/src/Controller/Stations/ProfileController.php +++ b/src/Controller/Stations/ProfileController.php @@ -200,6 +200,8 @@ final class ProfileController Response $response, int|string $station_id ): ResponseInterface { + $router = $request->getRouter(); + return $request->getView()->renderVuePage( response: $response, component: 'Vue_StationsProfileEdit', @@ -208,8 +210,8 @@ final class ProfileController props: array_merge( $this->stationFormComponent->getProps($request), [ - 'editUrl' => (string)$request->getRouter()->fromHere('api:stations:profile:edit'), - 'continueUrl' => (string)$request->getRouter()->fromHere('stations:profile:index'), + 'editUrl' => (string)$router->fromHere('api:stations:profile:edit'), + 'continueUrl' => (string)$router->fromHere('stations:profile:index'), ] ) ); diff --git a/src/Controller/Stations/UploadStereoToolConfigAction.php b/src/Controller/Stations/UploadStereoToolConfigAction.php new file mode 100644 index 000000000..c7f9f1ef8 --- /dev/null +++ b/src/Controller/Stations/UploadStereoToolConfigAction.php @@ -0,0 +1,33 @@ +getStation()->getBackendConfig(); + $router = $request->getRouter(); + + return $request->getView()->renderVuePage( + response: $response, + component: 'Vue_StationsStereoToolConfig', + id: 'stations-stereo-tool-config', + title: __('Upload Stereo Tool Configuration'), + props: [ + 'restartStatusUrl' => (string)$router->fromHere('api:stations:restart-status'), + 'recordHasStereoToolConfiguration' => !empty($backendConfig->getStereoToolConfigurationPath()), + 'apiUrl' => (string)$router->fromHere('api:stations:stereo_tool_config'), + ], + ); + } +} diff --git a/src/Entity/Repository/StationRepository.php b/src/Entity/Repository/StationRepository.php index 40f771863..9bc6993be 100644 --- a/src/Entity/Repository/StationRepository.php +++ b/src/Entity/Repository/StationRepository.php @@ -215,4 +215,55 @@ class StationRepository extends Repository $this->em->persist($station); $this->em->flush(); } + + public function setStereoToolConfiguration( + Entity\Station $station, + UploadedFile $file, + ?ExtendedFilesystemInterface $fs = null + ): void { + $fs ??= (new StationFilesystems($station))->getConfigFilesystem(); + + $backendConfig = $station->getBackendConfig(); + + if (null !== $backendConfig->getStereoToolConfigurationPath()) { + $this->doDeleteStereoToolConfiguration($station, $fs); + $backendConfig->setStereoToolConfigurationPath(null); + } + + $stereoToolConfigurationPath = 'stereo-tool.sts'; + $fs->uploadAndDeleteOriginal($file->getUploadedPath(), $stereoToolConfigurationPath); + + $backendConfig->setStereoToolConfigurationPath($stereoToolConfigurationPath); + $station->setBackendConfig($backendConfig); + + $this->em->persist($station); + $this->em->flush(); + } + + public function doDeleteStereoToolConfiguration( + Entity\Station $station, + ?ExtendedFilesystemInterface $fs = null + ): void { + $backendConfig = $station->getBackendConfig(); + if (null === $backendConfig->getStereoToolConfigurationPath()) { + return; + } + + $fs ??= (new StationFilesystems($station))->getConfigFilesystem(); + $fs->delete($backendConfig->getStereoToolConfigurationPath()); + } + + public function clearStereoToolConfiguration( + Entity\Station $station, + ?ExtendedFilesystemInterface $fs = null + ): void { + $this->doDeleteStereoToolConfiguration($station, $fs); + + $backendConfig = $station->getBackendConfig(); + $backendConfig->setStereoToolConfigurationPath(null); + $station->setBackendConfig($backendConfig); + + $this->em->persist($station); + $this->em->flush(); + } } diff --git a/src/Entity/StationBackendConfiguration.php b/src/Entity/StationBackendConfiguration.php index 3f1844943..4a0c9d0ad 100644 --- a/src/Entity/StationBackendConfiguration.php +++ b/src/Entity/StationBackendConfiguration.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Entity; use App\Entity\Enums\StationBackendPerformanceModes; +use App\Radio\Enums\AudioProcessingMethods; use App\Radio\Enums\StreamFormats; use Doctrine\Common\Collections\ArrayCollection; use InvalidArgumentException; @@ -124,16 +125,54 @@ class StationBackendConfiguration extends ArrayCollection $this->set(self::DJ_MOUNT_POINT, $mountPoint); } - public const USE_NORMALIZER = 'nrj'; + public const AUDIO_PROCESSING_METHOD = 'audio_processing_method'; - public function useNormalizer(): bool + public function getAudioProcessingMethod(): ?string { - return $this->get(self::USE_NORMALIZER) ?? false; + return $this->getAudioProcessingMethodEnum()->value; } - public function setUseNormalizer(?bool $useNormalizer): void + public function getAudioProcessingMethodEnum(): AudioProcessingMethods { - $this->set(self::USE_NORMALIZER, $useNormalizer); + return AudioProcessingMethods::tryFrom($this->get(self::AUDIO_PROCESSING_METHOD) ?? '') + ?? AudioProcessingMethods::default(); + } + + public function setAudioProcessingMethod(?string $method): void + { + if (null !== $method) { + $method = strtolower($method); + } + + if (null !== $method && null === AudioProcessingMethods::tryFrom($method)) { + throw new \InvalidArgumentException('Invalid audio processing method specified.'); + } + + $this->set(self::AUDIO_PROCESSING_METHOD, $method); + } + + public const STEREO_TOOL_LICENSE_KEY = 'stereo_tool_license_key'; + + public function getStereoToolLicenseKey(): ?string + { + return $this->get(self::STEREO_TOOL_LICENSE_KEY) ?? null; + } + + public function setStereoToolLicenseKey(?string $licenseKey): void + { + $this->set(self::STEREO_TOOL_LICENSE_KEY, $licenseKey); + } + + public const STEREO_TOOL_CONFIGURATION_PATH = 'stereo_tool_configuration_path'; + + public function getStereoToolConfigurationPath(): ?string + { + return $this->get(self::STEREO_TOOL_CONFIGURATION_PATH) ?? null; + } + + public function setStereoToolConfigurationPath(?string $stereoToolConfigurationPath): void + { + $this->set(self::STEREO_TOOL_CONFIGURATION_PATH, $stereoToolConfigurationPath); } public const USE_REPLAYGAIN = 'enable_replaygain_metadata'; diff --git a/src/OpenApi.php b/src/OpenApi.php index 563380bf8..36814ff33 100644 --- a/src/OpenApi.php +++ b/src/OpenApi.php @@ -32,6 +32,7 @@ use OpenApi\Attributes as OA; ), new OA\Tag(name: "Stations: General"), + new OA\Tag(name: "Stations: Broadcasting"), new OA\Tag(name: "Stations: Song Requests"), new OA\Tag(name: "Stations: Service Control"), new OA\Tag(name: "Stations: Automation"), diff --git a/src/Radio/Backend/Liquidsoap/ConfigWriter.php b/src/Radio/Backend/Liquidsoap/ConfigWriter.php index 2ff7d30cd..d3ca53d95 100644 --- a/src/Radio/Backend/Liquidsoap/ConfigWriter.php +++ b/src/Radio/Backend/Liquidsoap/ConfigWriter.php @@ -8,11 +8,13 @@ use App\Entity; use App\Environment; use App\Event\Radio\WriteLiquidsoapConfiguration; use App\Radio\Backend\Liquidsoap; +use App\Radio\Enums\AudioProcessingMethods; use App\Radio\Enums\FrontendAdapters; use App\Radio\Enums\LiquidsoapQueues; use App\Radio\Enums\StreamFormats; use App\Radio\Enums\StreamProtocols; use App\Radio\FallbackFile; +use App\Radio\StereoTool; use Carbon\CarbonImmutable; use Doctrine\ORM\EntityManagerInterface; use Psr\EventDispatcher\EventDispatcherInterface; @@ -36,7 +38,8 @@ class ConfigWriter implements EventSubscriberInterface protected Environment $environment, protected LoggerInterface $logger, protected EventDispatcherInterface $eventDispatcher, - protected FallbackFile $fallbackFile + protected FallbackFile $fallbackFile, + protected StereoTool $stereoTool, ) { } @@ -868,7 +871,7 @@ class ConfigWriter implements EventSubscriberInterface ); // NRJ normalization - if ($settings->useNormalizer()) { + if (AudioProcessingMethods::Liquidsoap === $settings->getAudioProcessingMethodEnum()) { $event->appendBlock( <<getAudioProcessingMethodEnum() + && $this->stereoTool->isReady($station) + ) { + $stereoToolBinary = $this->stereoTool->getBinaryPath(); + + $stereoToolConfiguration = $station->getRadioConfigDir() + . DIRECTORY_SEPARATOR . $settings->getStereoToolConfigurationPath(); + $stereoToolProcess = $stereoToolBinary . ' --silent - - -s ' . $stereoToolConfiguration; + + $stereoToolLicenseKey = $settings->getStereoToolLicenseKey(); + if (!empty($stereoToolLicenseKey)) { + $stereoToolProcess .= ' -k "' . $stereoToolLicenseKey . '"'; + } + + $event->appendBlock( + <<useReplayGain()) { $event->appendBlock( diff --git a/src/Radio/Enums/AudioProcessingMethods.php b/src/Radio/Enums/AudioProcessingMethods.php new file mode 100644 index 000000000..179a6b2a8 --- /dev/null +++ b/src/Radio/Enums/AudioProcessingMethods.php @@ -0,0 +1,28 @@ +value; + } + + public static function default(): self + { + return self::None; + } +} diff --git a/src/Radio/StereoTool.php b/src/Radio/StereoTool.php new file mode 100644 index 000000000..71bca84c4 --- /dev/null +++ b/src/Radio/StereoTool.php @@ -0,0 +1,57 @@ +getBinaryPath()); + } + + public function getBinaryPath(): string + { + return $this->environment->getParentDirectory() . '/servers/stereo_tool/stereo_tool'; + } + + public function isReady(Entity\Station $station): bool + { + if (!$this->isInstalled()) { + return false; + } + + $backendConfig = $station->getBackendConfig(); + return !empty($backendConfig->getStereoToolConfigurationPath()); + } + + public function getVersion(): ?string + { + if (!$this->isInstalled()) { + return null; + } + + $binaryPath = $this->getBinaryPath(); + + $process = new Process([$binaryPath, '--help']); + $process->setWorkingDirectory(dirname($binaryPath)); + $process->run(); + + if (!$process->isSuccessful()) { + return null; + } + + preg_match('/STEREO TOOL ([.\d]+) CONSOLE APPLICATION/i', $process->getErrorOutput(), $matches); + return $matches[1] ?? null; + } +} diff --git a/src/VueComponent/StationFormComponent.php b/src/VueComponent/StationFormComponent.php index f7055556d..70044af83 100644 --- a/src/VueComponent/StationFormComponent.php +++ b/src/VueComponent/StationFormComponent.php @@ -9,6 +9,7 @@ use App\Enums\GlobalPermissions; use App\Http\ServerRequest; use App\Radio\Adapters; use App\Radio\Enums\FrontendAdapters; +use App\Radio\StereoTool; use DateTime; use DateTimeZone; use Symfony\Component\Intl\Countries; @@ -17,6 +18,7 @@ class StationFormComponent implements VueComponentInterface { public function __construct( protected Adapters $adapters, + protected StereoTool $stereoTool, protected SettingsRepository $settingsRepo ) { } @@ -32,6 +34,7 @@ class StationFormComponent implements VueComponentInterface 'showAdvanced' => $settings->getEnableAdvancedFeatures(), 'timezones' => $this->getTimezones(), 'isShoutcastInstalled' => isset($installedFrontends[FrontendAdapters::Shoutcast->value]), + 'isStereoToolInstalled' => $this->stereoTool->isInstalled(), 'countries' => Countries::getNames(), 'storageLocationApiUrl' => (string)$request->getRouter()->named('api:admin:stations:storage-locations'), ]; diff --git a/util/ansible/roles/azuracast-config/tasks/main.yml b/util/ansible/roles/azuracast-config/tasks/main.yml index 62b11eddb..d82ae8656 100644 --- a/util/ansible/roles/azuracast-config/tasks/main.yml +++ b/util/ansible/roles/azuracast-config/tasks/main.yml @@ -46,6 +46,7 @@ - "{{ app_base }}/servers" - "{{ app_base }}/servers/shoutcast2" - "{{ app_base }}/servers/icecast2" + - "{{ app_base }}/servers/stereo_tool" - "{{ app_base }}/uploads" loop_control: loop_var: azuracast_config_sys_directory diff --git a/util/docker/common/add_user.sh b/util/docker/common/add_user.sh index a0ba41907..b84db1de1 100644 --- a/util/docker/common/add_user.sh +++ b/util/docker/common/add_user.sh @@ -14,7 +14,7 @@ usermod -aG docker_env azuracast usermod -aG www-data azuracast mkdir -p /var/azuracast/www /var/azuracast/stations /var/azuracast/servers/shoutcast2 \ - /var/azuracast/backups /var/azuracast/www_tmp \ + /var/azuracast/servers/stereo_tool /var/azuracast/backups /var/azuracast/www_tmp \ /var/azuracast/uploads /var/azuracast/geoip /var/azuracast/dbip chown -R azuracast:azuracast /var/azuracast diff --git a/util/docker/mariadb/setup/mariadb.sh b/util/docker/mariadb/setup/mariadb.sh index c1687125d..38280b208 100644 --- a/util/docker/mariadb/setup/mariadb.sh +++ b/util/docker/mariadb/setup/mariadb.sh @@ -5,9 +5,8 @@ set -x $minimal_apt_get_install tzdata libjemalloc2 pwgen xz-utils zstd dirmngr apt-transport-https -sudo apt-get install software-properties-common sudo apt-key adv --fetch-keys 'https://mariadb.org/mariadb_release_signing_key.asc' -sudo add-apt-repository 'deb [arch=amd64,arm64,ppc64el,s390x] https://mirrors.gigenet.com/mariadb/repo/10.7/ubuntu focal main' +sudo add-apt-repository 'deb [arch=amd64,arm64,ppc64el,s390x] https://atl.mirrors.knownhost.com/mariadb/repo/10.7/ubuntu focal main' # Pulled from MariaDB Docker container export MARIADB_MAJOR=10.7