feat: add stereo tool support for stations (#5344)
Co-authored-by: Buster "Silver Eagle" Neece <buster@busterneece.com> Co-authored-by: Vaalyn <vaalyndev@gmail.com>
This commit is contained in:
parent
f80682da5c
commit
4371ac3be3
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
],
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: { }
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
<translate :key="lang">AutoDJ Service</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">This software shuffles from playlists of music constantly and plays when no other radio source is available.</translate>
|
||||
<translate :key="lang">This software shuffles from playlists of music constantly and plays when
|
||||
no other radio source is available.
|
||||
</translate>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-radio-group stacked :id="props.id" :options="backendTypeOptions"
|
||||
|
@ -28,7 +30,10 @@
|
|||
<translate :key="lang">Crossfade Method</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">Choose a method to use when transitioning from one song to another. Smart Mode considers the volume of the two tracks when fading for a smoother effect, but requires more CPU resources.</translate>
|
||||
<translate :key="lang">Choose a method to use when transitioning from one song to another.
|
||||
Smart Mode considers the volume of the two tracks when fading for a smoother effect, but
|
||||
requires more CPU resources.
|
||||
</translate>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-radio-group stacked :id="props.id" :options="crossfadeOptions"
|
||||
|
@ -47,26 +52,71 @@
|
|||
<translate :key="lang">Number of seconds to overlap songs.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-checkbox class="col-md-12"
|
||||
id="edit_form_backend_config_nrj"
|
||||
:field="form.backend_config.nrj">
|
||||
</b-form-row>
|
||||
<b-form-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="edit_form_backend_config_audio_processing_method"
|
||||
:field="form.backend_config.audio_processing_method">
|
||||
<template #label="{lang}">
|
||||
<translate :key="lang">Apply Compression and Normalization</translate>
|
||||
<translate :key="lang">Audio Processing Method</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">Compress and normalize your station's audio, producing a more uniform and "full" sound.</translate>
|
||||
<translate :key="lang">Choose a method to use for processing audio which produces a more
|
||||
uniform and "full" sound for your station.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-checkbox>
|
||||
<template #default="props">
|
||||
<b-form-radio-group stacked :id="props.id" :options="audioProcessingOptions"
|
||||
v-model="props.field.$model">
|
||||
</b-form-radio-group>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-form-row>
|
||||
</b-form-fieldset>
|
||||
|
||||
<b-form-fieldset v-if="isStereoToolEnabled && isStereoToolInstalled">
|
||||
<template #label>
|
||||
<translate key="lang_hdr_stereo_tool">Stereo Tool</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_stereo_tool_desc">Stereo Tool is an industry standard for software audio processing. For more information on how to configure it, please refer to the</translate>
|
||||
<a href="https://www.thimeo.com/stereo-tool/" target="_blank">
|
||||
<translate key="lang_stereo_tool_documentation_desc">Stereo Tool documentation.</translate>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<b-form-fieldset>
|
||||
<b-form-row>
|
||||
<b-wrapped-form-group class="col-md-7" id="edit_form_backend_stereo_tool_license_key"
|
||||
:field="form.backend_config.stereo_tool_license_key" input-type="text">
|
||||
<template #label="{lang}">
|
||||
<translate :key="lang">Stereo Tool License Key</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">Provide a valid license key from Thimeo. Functionality is limited without a license key.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-form-markup class="col-md-5" id="edit_form_backend_stereo_tool_config">
|
||||
<template #label="{lang}">
|
||||
<translate :key="lang">Upload Stereo Tool Configuration</translate>
|
||||
</template>
|
||||
|
||||
<p class="card-text">
|
||||
<translate key="lang_stereotool_config">Upload a Stereo Tool configuration file from the "Broadcasting" submenu in the station profile.</translate>
|
||||
</p>
|
||||
</b-form-markup>
|
||||
</b-form-row>
|
||||
</b-form-fieldset>
|
||||
</b-form-fieldset>
|
||||
|
||||
<b-form-fieldset>
|
||||
<template #label>
|
||||
<translate key="lang_hdr_song_requests">Song Requests</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_song_requests_desc">Some stream licensing providers may have specific rules regarding song requests. Check your local regulations for more information.</translate>
|
||||
<translate key="lang_song_requests_desc">Some stream licensing providers may have specific rules
|
||||
regarding song requests. Check your local regulations for more information.
|
||||
</translate>
|
||||
</template>
|
||||
|
||||
<b-form-fieldset>
|
||||
|
@ -77,7 +127,9 @@
|
|||
<translate :key="lang">Allow Song Requests</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">Enable listeners to request a song for play on your station. Only songs that are already in your playlists are requestable.</translate>
|
||||
<translate :key="lang">Enable listeners to request a song for play on your station. Only
|
||||
songs that are already in your playlists are requestable.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-checkbox>
|
||||
</b-form-row>
|
||||
|
@ -92,7 +144,10 @@
|
|||
<translate :key="lang">Request Minimum Delay (Minutes)</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">If requests are enabled, this specifies the minimum delay (in minutes) between a request being submitted and being played. If set to zero, a minor delay of 15 seconds is applied to prevent request floods.</translate>
|
||||
<translate :key="lang">If requests are enabled, this specifies the minimum delay (in
|
||||
minutes) between a request being submitted and being played. If set to zero, a minor
|
||||
delay of 15 seconds is applied to prevent request floods.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
|
@ -103,7 +158,10 @@
|
|||
<translate :key="lang">Request Last Played Threshold (Minutes)</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">This specifies the minimum time (in minutes) between a song playing on the radio and being available to request again. Set to 0 for no threshold.</translate>
|
||||
<translate :key="lang">This specifies the minimum time (in minutes) between a song
|
||||
playing on the radio and being available to request again. Set to 0 for no
|
||||
threshold.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-form-row>
|
||||
|
@ -123,7 +181,9 @@
|
|||
<translate :key="lang">Allow Streamers / DJs</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">If enabled, streamers (or DJs) will be able to connect directly to your stream and broadcast live music that interrupts the AutoDJ stream.</translate>
|
||||
<translate :key="lang">If enabled, streamers (or DJs) will be able to connect directly
|
||||
to your stream and broadcast live music that interrupts the AutoDJ stream.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-checkbox>
|
||||
</b-form-row>
|
||||
|
@ -138,7 +198,9 @@
|
|||
<translate :key="lang">Record Live Broadcasts</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">If enabled, AzuraCast will automatically record any live broadcasts made to this station to per-broadcast recordings.</translate>
|
||||
<translate :key="lang">If enabled, AzuraCast will automatically record any live
|
||||
broadcasts made to this station to per-broadcast recordings.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-checkbox>
|
||||
</b-form-row>
|
||||
|
@ -183,20 +245,28 @@
|
|||
<translate :key="lang">Deactivate Streamer on Disconnect (Seconds)</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">This is the number of seconds until a streamer who has been manually disconnected can reconnect to the stream. Set to 0 to allow the streamer to immediately reconnect.</translate>
|
||||
<translate :key="lang">This is the number of seconds until a streamer who has been
|
||||
manually disconnected can reconnect to the stream. Set to 0 to allow the
|
||||
streamer to immediately reconnect.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group v-if="showAdvanced" class="col-md-6" id="edit_form_backend_dj_port"
|
||||
<b-wrapped-form-group v-if="showAdvanced" class="col-md-6"
|
||||
id="edit_form_backend_dj_port"
|
||||
:field="form.backend_config.dj_port" input-type="number"
|
||||
:input-attrs="{ min: '0' }" advanced>
|
||||
<template #label="{lang}">
|
||||
<translate :key="lang">Customize DJ/Streamer Port</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">No other program can be using this port. Leave blank to automatically assign a port.</translate>
|
||||
<translate :key="lang">No other program can be using this port. Leave blank to
|
||||
automatically assign a port.
|
||||
</translate>
|
||||
<br>
|
||||
<translate :key="lang+'2'">Note: the port after this one will automatically be used for legacy connections.</translate>
|
||||
<translate :key="lang+'2'">Note: the port after this one will automatically be used
|
||||
for legacy connections.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
|
@ -207,7 +277,10 @@
|
|||
<translate :key="lang">DJ/Streamer Buffer Time (Seconds)</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">The number of seconds of signal to store in case of interruption. Set to the lowest value that your DJs can use without stream interruptions.</translate>
|
||||
<translate :key="lang">The number of seconds of signal to store in case of
|
||||
interruption. Set to the lowest value that your DJs can use without stream
|
||||
interruptions.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
|
@ -218,7 +291,9 @@
|
|||
<translate :key="lang">Customize DJ/Streamer Mount Point</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">If your streaming software requires a specific mount point path, specify it here. Otherwise, use the default.</translate>
|
||||
<translate :key="lang">If your streaming software requires a specific mount point
|
||||
path, specify it here. Otherwise, use the default.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-form-row>
|
||||
|
@ -239,7 +314,10 @@
|
|||
<translate :key="lang">Manual AutoDJ Mode</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">This mode disables AzuraCast's AutoDJ management, using Liquidsoap itself to manage song playback. "Next Song" and some other features will not be available.</translate>
|
||||
<translate :key="lang">This mode disables AzuraCast's AutoDJ management, using Liquidsoap
|
||||
itself to manage song playback. "Next Song" and some other features will not be
|
||||
available.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-checkbox>
|
||||
|
||||
|
@ -250,7 +328,9 @@
|
|||
<translate :key="lang">Use Replaygain Metadata</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">Instruct Liquidsoap to use any replaygain metadata associated with a song to control its volume level. This may increase CPU consumption.</translate>
|
||||
<translate :key="lang">Instruct Liquidsoap to use any replaygain metadata associated with a
|
||||
song to control its volume level. This may increase CPU consumption.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-checkbox>
|
||||
|
||||
|
@ -261,7 +341,9 @@
|
|||
<translate :key="lang">Customize Internal Request Processing Port</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">This port is not used by any external process. Only modify this port if the assigned port is in use. Leave blank to automatically assign a port.</translate>
|
||||
<translate :key="lang">This port is not used by any external process. Only modify this port
|
||||
if the assigned port is in use. Leave blank to automatically assign a port.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
|
@ -272,7 +354,9 @@
|
|||
<translate :key="lang">AutoDJ Queue Length</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">This determines how many songs in advance the AutoDJ will automatically fill the queue.</translate>
|
||||
<translate :key="lang">This determines how many songs in advance the AutoDJ will
|
||||
automatically fill the queue.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
|
@ -282,7 +366,10 @@
|
|||
<translate :key="lang">Character Set Encoding</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">For most cases, use the default UTF-8 encoding. The older ISO-8859-1 encoding can be used if accepting connections from SHOUTcast 1 DJs or using other legacy software.</translate>
|
||||
<translate :key="lang">For most cases, use the default UTF-8 encoding. The older ISO-8859-1
|
||||
encoding can be used if accepting connections from SHOUTcast 1 DJs or using other legacy
|
||||
software.
|
||||
</translate>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-radio-group stacked :id="props.id" :options="charsetOptions"
|
||||
|
@ -297,7 +384,9 @@
|
|||
<translate :key="lang">Liquidsoap Performance Tuning</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">If your installation is constrained by CPU or memory, you can change this setting to tune the resources used by Liquidsoap.</translate>
|
||||
<translate :key="lang">If your installation is constrained by CPU or memory, you can change
|
||||
this setting to tune the resources used by Liquidsoap.
|
||||
</translate>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-radio-group stacked :id="props.id" :options="performanceModeOptions"
|
||||
|
@ -313,29 +402,41 @@
|
|||
<translate :key="lang">Duplicate Prevention Time Range (Minutes)</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate :key="lang">This specifies the time range (in minutes) of the song history that the duplicate song prevention algorithm should take into account.</translate>
|
||||
<translate :key="lang">This specifies the time range (in minutes) of the song history that
|
||||
the duplicate song prevention algorithm should take into account.
|
||||
</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-form-row>
|
||||
</b-form-fieldset>
|
||||
|
||||
</b-form-fieldset>
|
||||
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BFormFieldset from "~/components/Form/BFormFieldset";
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
import {BACKEND_LIQUIDSOAP, BACKEND_NONE} from "~/components/Entity/RadioAdapters";
|
||||
import {
|
||||
AUDIO_PROCESSING_LIQUIDSOAP,
|
||||
AUDIO_PROCESSING_NONE,
|
||||
AUDIO_PROCESSING_STEREO_TOOL,
|
||||
BACKEND_LIQUIDSOAP,
|
||||
BACKEND_NONE
|
||||
} from "~/components/Entity/RadioAdapters";
|
||||
import BWrappedFormCheckbox from "~/components/Form/BWrappedFormCheckbox";
|
||||
import BFormMarkup from "~/components/Form/BFormMarkup";
|
||||
|
||||
export default {
|
||||
name: 'AdminStationsBackendForm',
|
||||
components: {BWrappedFormCheckbox, BWrappedFormGroup, BFormFieldset},
|
||||
components: {BFormMarkup, BWrappedFormCheckbox, BWrappedFormGroup, BFormFieldset},
|
||||
props: {
|
||||
form: Object,
|
||||
station: Object,
|
||||
tabClass: {},
|
||||
isStereoToolInstalled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showAdvanced: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
|
@ -360,6 +461,9 @@ export default {
|
|||
isBackendEnabled() {
|
||||
return this.form.backend_type.$model !== BACKEND_NONE;
|
||||
},
|
||||
isStereoToolEnabled() {
|
||||
return this.form.backend_config.audio_processing_method.$model === AUDIO_PROCESSING_STEREO_TOOL;
|
||||
},
|
||||
crossfadeOptions() {
|
||||
return [
|
||||
{
|
||||
|
@ -376,6 +480,29 @@ export default {
|
|||
}
|
||||
];
|
||||
},
|
||||
audioProcessingOptions() {
|
||||
const audioProcessingOptions = [
|
||||
{
|
||||
text: this.$gettext('Liquidsoap'),
|
||||
value: AUDIO_PROCESSING_LIQUIDSOAP,
|
||||
},
|
||||
{
|
||||
text: this.$gettext('Disable Processing'),
|
||||
value: AUDIO_PROCESSING_NONE,
|
||||
}
|
||||
];
|
||||
|
||||
if (this.isStereoToolInstalled) {
|
||||
audioProcessingOptions.splice(1, 0,
|
||||
{
|
||||
text: this.$gettext('Stereo Tool'),
|
||||
value: AUDIO_PROCESSING_STEREO_TOOL,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return audioProcessingOptions;
|
||||
},
|
||||
recordStreamsOptions() {
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
:is-shoutcast-installed="isShoutcastInstalled"
|
||||
:countries="countries"
|
||||
:show-advanced="showAdvanced"></admin-stations-frontend-form>
|
||||
<admin-stations-backend-form :form="$v.form" :tab-class="getTabClass($v.backendTab)"
|
||||
<admin-stations-backend-form :form="$v.form" :station="station" :tab-class="getTabClass($v.backendTab)"
|
||||
:is-stereo-tool-installed="isStereoToolInstalled"
|
||||
:show-advanced="showAdvanced"></admin-stations-backend-form>
|
||||
<admin-stations-admin-form v-if="showAdminTab" :tab-class="getTabClass($v.adminTab)" :form="$v.form"
|
||||
:is-edit-mode="isEditMode" :storage-location-api-url="storageLocationApiUrl"
|
||||
|
@ -35,7 +36,7 @@
|
|||
<script>
|
||||
import {validationMixin} from "vuelidate";
|
||||
import {decimal, numeric, required, url} from 'vuelidate/dist/validators.min.js';
|
||||
import {BACKEND_LIQUIDSOAP, FRONTEND_ICECAST} from "~/components/Entity/RadioAdapters";
|
||||
import {AUDIO_PROCESSING_NONE, BACKEND_LIQUIDSOAP, FRONTEND_ICECAST} from "~/components/Entity/RadioAdapters";
|
||||
import AdminStationsProfileForm from "./Form/ProfileForm";
|
||||
import AdminStationsFrontendForm from "./Form/FrontendForm";
|
||||
import AdminStationsBackendForm from "./Form/BackendForm";
|
||||
|
@ -61,6 +62,10 @@ export const StationFormProps = {
|
|||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isStereoToolInstalled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
countries: Object,
|
||||
// Admin
|
||||
storageLocationApiUrl: String
|
||||
|
@ -108,7 +113,8 @@ export default {
|
|||
backend_config: {
|
||||
crossfade_type: {},
|
||||
crossfade: {decimal},
|
||||
nrj: {},
|
||||
audio_processing_method: {},
|
||||
stereo_tool_license_key: {},
|
||||
record_streams: {},
|
||||
record_streams_format: {},
|
||||
record_streams_bitrate: {},
|
||||
|
@ -205,6 +211,12 @@ export default {
|
|||
return {
|
||||
loading: true,
|
||||
error: null,
|
||||
station: {
|
||||
stereo_tool_configuration_file_path: null,
|
||||
links: {
|
||||
stereo_tool_configuration: null
|
||||
}
|
||||
},
|
||||
form: {}
|
||||
};
|
||||
},
|
||||
|
@ -258,7 +270,9 @@ export default {
|
|||
backend_config: {
|
||||
crossfade_type: 'normal',
|
||||
crossfade: 2,
|
||||
nrj: false,
|
||||
audio_processing_method: AUDIO_PROCESSING_NONE,
|
||||
stereo_tool_license_key: '',
|
||||
stereo_tool_configuration_file: null,
|
||||
record_streams: false,
|
||||
record_streams_format: 'mp3',
|
||||
record_streams_bitrate: 128,
|
||||
|
@ -316,6 +330,12 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
this.station = {
|
||||
stereo_tool_configuration_file_path: null,
|
||||
links: {
|
||||
stereo_tool_configuration: null
|
||||
}
|
||||
};
|
||||
this.form = form;
|
||||
},
|
||||
reset() {
|
||||
|
@ -336,6 +356,7 @@ export default {
|
|||
});
|
||||
},
|
||||
populateForm(data) {
|
||||
this.record = data;
|
||||
this.form = mergeExisting(this.form, data);
|
||||
},
|
||||
getSubmittableFormData() {
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title">
|
||||
<translate key="lang_title">Install Stereo Tool</translate>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<b-overlay variant="card" :show="loading">
|
||||
<b-form-row>
|
||||
<div class="col-md-7">
|
||||
<fieldset>
|
||||
<legend>
|
||||
<translate key="lang_instructions">Instructions</translate>
|
||||
</legend>
|
||||
|
||||
<p class="card-text">
|
||||
<translate key="lang_instructions_1a">Stereo Tool is not free software, and its restrictive license does not allow AzuraCast to distribute the Stereo Tool binary.</translate>
|
||||
</p>
|
||||
|
||||
<p class="card-text">
|
||||
<translate key="lang_instructions_1b">In order to install Stereo Tool:</translate>
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<translate key="lang_instructions_2">Download the appropriate binary from the Stereo Tool downloads page:</translate>
|
||||
<br>
|
||||
<a href="https://www.thimeo.com/stereo-tool/download/"
|
||||
target="_blank">
|
||||
<translate key="lang_instructions_2_url">Stereo Tool Downloads</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<translate key="lang_instructions_3">For most installations, you should choose the "Command line version 64 bit". For Raspberry Pi devices, select "Raspberry Pi 3/4 64 bit command line".</translate>
|
||||
</li>
|
||||
<li>
|
||||
<translate key="lang_instructions_4">Upload the file on this page to automatically extract it into the proper directory.</translate>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<fieldset class="mb-3">
|
||||
<legend>
|
||||
<translate key="lang_current_version">Current Installed Version</translate>
|
||||
</legend>
|
||||
|
||||
<p v-if="version" class="text-success card-text">
|
||||
{{ langInstalledVersion }}
|
||||
</p>
|
||||
<p v-else class="text-danger card-text">
|
||||
<translate
|
||||
key="lang_not_installed">Stereo Tool is not currently installed on this installation.</translate>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<flow-upload :target-url="apiUrl" @complete="relist"></flow-upload>
|
||||
</div>
|
||||
</b-form-row>
|
||||
</b-overlay>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FlowUpload from "~/components/Common/FlowUpload";
|
||||
|
||||
export default {
|
||||
name: 'AdminStereoTool',
|
||||
components: {FlowUpload},
|
||||
props: {
|
||||
apiUrl: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
version: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
langInstalledVersion() {
|
||||
const text = this.$gettext('Stereo Tool version %{ version } is currently installed.');
|
||||
return this.$gettextInterpolate(text, {
|
||||
version: this.version
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.relist();
|
||||
},
|
||||
methods: {
|
||||
relist() {
|
||||
this.loading = true;
|
||||
this.axios.get(this.apiUrl).then((resp) => {
|
||||
this.version = resp.data.version;
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<section class="card" role="region">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title">
|
||||
<translate key="lang_hdr">Upload Stereo Tool Configuration</translate>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<info-card>
|
||||
<p class="card-text">
|
||||
<translate key="lang_stereo_tool_desc">Stereo Tool is an industry standard for software audio processing. For more information on how to configure it, please refer to the</translate>
|
||||
<a href="https://www.thimeo.com/stereo-tool/" target="_blank">
|
||||
<translate key="lang_stereo_tool_documentation_desc">Stereo Tool documentation.</translate>
|
||||
</a>
|
||||
</p>
|
||||
</info-card>
|
||||
|
||||
<div class="card-body">
|
||||
<b-form-group>
|
||||
<b-form-row>
|
||||
<b-form-group class="col-md-6" label-for="stereo_tool_configuration_file">
|
||||
<template #label>
|
||||
<translate key="stereo_tool_configuration_file">Select Configuration File</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="stereo_tool_configuration_file_desc">This configuration file should be a valid .sts file exported from Stereo Tool.</translate>
|
||||
</template>
|
||||
|
||||
<flow-upload :target-url="apiUrl" :valid-mime-types="acceptMimeTypes"
|
||||
@success="onFileSuccess"></flow-upload>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group class="col-md-6">
|
||||
<template #label>
|
||||
<translate key="existing_stereo_tool_configuration">Current Configuration File</translate>
|
||||
</template>
|
||||
<div v-if="hasStereoToolConfiguration">
|
||||
<div class="buttons pt-3">
|
||||
<b-button block variant="bg" :href="apiUrl" target="_blank">
|
||||
<translate key="btn_download">Download</translate>
|
||||
</b-button>
|
||||
<b-button block variant="danger" @click="deleteConfigurationFile">
|
||||
<translate key="btn_delete_stereo_tool_configuration">Clear File</translate>
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<translate key="no_existing_stereo_tool_configuration">There is no Stereo Tool configuration file present.</translate>
|
||||
</div>
|
||||
</b-form-group>
|
||||
</b-form-row>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FlowUpload from '~/components/Common/FlowUpload';
|
||||
import InfoCard from "~/components/Common/InfoCard";
|
||||
|
||||
export default {
|
||||
name: 'StationsStereoToolConfiguration',
|
||||
components: {InfoCard, FlowUpload},
|
||||
props: {
|
||||
restartStatusUrl: String,
|
||||
recordHasStereoToolConfiguration: Boolean,
|
||||
apiUrl: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasStereoToolConfiguration: this.recordHasStereoToolConfiguration,
|
||||
acceptMimeTypes: ['text/plain']
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onFileSuccess(file, message) {
|
||||
this.mayNeedRestart();
|
||||
this.hasStereoToolConfiguration = true;
|
||||
},
|
||||
deleteConfigurationFile() {
|
||||
this.$wrapWithLoading(
|
||||
this.axios({
|
||||
method: 'DELETE',
|
||||
url: this.apiUrl
|
||||
})
|
||||
).then((resp) => {
|
||||
this.mayNeedRestart();
|
||||
this.hasStereoToolConfiguration = false;
|
||||
this.$notifySuccess();
|
||||
});
|
||||
},
|
||||
mayNeedRestart() {
|
||||
this.axios.get(this.restartStatusUrl).then((resp) => {
|
||||
if (resp.data.needs_restart) {
|
||||
document.dispatchEvent(new CustomEvent("station-needs-restart"));
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
import initBase from '~/base.js';
|
||||
|
||||
import '~/vendor/bootstrapVue.js';
|
||||
|
||||
import AdminStereoTool from '~/components/Admin/StereoTool.vue';
|
||||
|
||||
export default initBase(AdminStereoTool);
|
|
@ -0,0 +1,7 @@
|
|||
import initBase from '~/base.js';
|
||||
|
||||
import '~/vendor/bootstrapVue.js';
|
||||
|
||||
import StereoToolConfig from '~/components/Stations/StereoToolConfig.vue';
|
||||
|
||||
export default initBase(StereoToolConfig);
|
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class StereoToolAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response
|
||||
): ResponseInterface {
|
||||
$router = $request->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'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Admin\StereoTool;
|
||||
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\StereoTool;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class GetAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StereoTool $stereoTool,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response
|
||||
): ResponseInterface {
|
||||
return $response->withJson(
|
||||
[
|
||||
'success' => true,
|
||||
'version' => $this->stereoTool->getVersion(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Admin\StereoTool;
|
||||
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\StereoTool;
|
||||
use App\Service\Flow;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class PostAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StereoTool $stereoTool
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
): ResponseInterface {
|
||||
$flowResponse = Flow::process($request, $response);
|
||||
if ($flowResponse instanceof ResponseInterface) {
|
||||
return $flowResponse;
|
||||
}
|
||||
|
||||
$binaryPath = $this->stereoTool->getBinaryPath();
|
||||
if (is_file($binaryPath)) {
|
||||
unlink($binaryPath);
|
||||
}
|
||||
|
||||
$flowResponse->moveTo($binaryPath);
|
||||
|
||||
chmod($binaryPath, 0744);
|
||||
|
||||
return $response->withJson(Entity\Api\Status::success());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\StereoTool;
|
||||
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/station/{station_id}/stereo-tool-configuration',
|
||||
description: 'Removes the Stereo Tool configuration file for a station.',
|
||||
security: OpenApi::API_KEY_SECURITY,
|
||||
tags: ['Stations: Broadcasting'],
|
||||
parameters: [
|
||||
new OA\Parameter(ref: OpenApi::REF_STATION_ID_REQUIRED),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_SUCCESS, response: 200),
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_ACCESS_DENIED, response: 403),
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_NOT_FOUND, response: 404),
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_GENERIC_ERROR, response: 500),
|
||||
]
|
||||
)]
|
||||
final class DeleteStereoToolConfigurationAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Entity\Repository\StationRepository $stationRepo
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int|string $station_id
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
$this->stationRepo->clearStereoToolConfiguration($station);
|
||||
|
||||
return $response->withJson(Entity\Api\Status::deleted());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\StereoTool;
|
||||
|
||||
use App\Entity;
|
||||
use App\Flysystem\StationFilesystems;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
#[OA\Get(
|
||||
path: '/station/{station_id}/stereo-tool-configuration',
|
||||
description: 'Get the Stereo Tool configuration file for a station.',
|
||||
security: OpenApi::API_KEY_SECURITY,
|
||||
tags: ['Stations: Broadcasting'],
|
||||
parameters: [
|
||||
new OA\Parameter(ref: OpenApi::REF_STATION_ID_REQUIRED),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Success'
|
||||
),
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_ACCESS_DENIED, response: 403),
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_NOT_FOUND, response: 404),
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_GENERIC_ERROR, response: 500),
|
||||
]
|
||||
)]
|
||||
final class GetStereoToolConfigurationAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int|string $station_id
|
||||
): ResponseInterface {
|
||||
set_time_limit(600);
|
||||
|
||||
$station = $request->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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\StereoTool;
|
||||
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use App\Service\Flow;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
#[OA\Post(
|
||||
path: '/station/{station_id}/stereo-tool-configuration',
|
||||
description: 'Update the Stereo Tool configuration file for a station.',
|
||||
security: OpenApi::API_KEY_SECURITY,
|
||||
tags: ['Stations: Broadcasting'],
|
||||
parameters: [
|
||||
new OA\Parameter(ref: OpenApi::REF_STATION_ID_REQUIRED),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_SUCCESS, response: 200),
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_ACCESS_DENIED, response: 403),
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_NOT_FOUND, response: 404),
|
||||
new OA\Response(ref: OpenApi::REF_RESPONSE_GENERIC_ERROR, response: 500),
|
||||
]
|
||||
)]
|
||||
final class PostStereoToolConfigurationAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Entity\Repository\StationRepository $stationRepo
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int|string $station_id
|
||||
): ResponseInterface {
|
||||
$station = $request->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());
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Stations;
|
||||
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class UploadStereoToolConfigAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int|string $station_id
|
||||
): ResponseInterface {
|
||||
$backendConfig = $request->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'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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(
|
||||
<<<EOF
|
||||
# Normalization and Compression
|
||||
|
@ -878,6 +881,30 @@ class ConfigWriter implements EventSubscriberInterface
|
|||
);
|
||||
}
|
||||
|
||||
// Stereo Tool processing
|
||||
if (
|
||||
AudioProcessingMethods::StereoTool === $settings->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(
|
||||
<<<EOF
|
||||
# Stereo Tool Pipe
|
||||
radio = pipe(replay_delay=1.0, process='{$stereoToolProcess}', radio)
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
// Replaygain metadata
|
||||
if ($settings->useReplayGain()) {
|
||||
$event->appendBlock(
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
// phpcs:ignoreFile
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Radio\Enums;
|
||||
|
||||
use App\Radio\Frontend\Icecast;
|
||||
use App\Radio\Frontend\Remote;
|
||||
use App\Radio\Frontend\Shoutcast;
|
||||
|
||||
enum AudioProcessingMethods: string
|
||||
{
|
||||
case Liquidsoap = 'nrj';
|
||||
case StereoTool = 'stereo_tool';
|
||||
case None = 'none';
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public static function default(): self
|
||||
{
|
||||
return self::None;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Radio;
|
||||
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class StereoTool
|
||||
{
|
||||
public function __construct(
|
||||
protected Environment $environment,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
return file_exists($this->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;
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue