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:
Bjarn Bronsveld 2022-05-23 06:50:55 +02:00 committed by GitHub
parent f80682da5c
commit 4371ac3be3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 993 additions and 58 deletions

View File

@ -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

View File

@ -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,
],
],

View File

@ -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',

View File

@ -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');
}

View File

@ -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));

View File

@ -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}',

View File

@ -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) {

View File

@ -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: { }

View File

@ -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 [
{

View File

@ -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() {

View File

@ -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>

View File

@ -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';

View File

@ -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>

View File

@ -0,0 +1,7 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import AdminStereoTool from '~/components/Admin/StereoTool.vue';
export default initBase(AdminStereoTool);

View File

@ -0,0 +1,7 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import StereoToolConfig from '~/components/Stations/StereoToolConfig.vue';
export default initBase(StereoToolConfig);

View File

@ -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',

View File

@ -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'),
],
);
}
}

View File

@ -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(),
]
);
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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'),
]
)
);

View File

@ -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'),
],
);
}
}

View File

@ -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();
}
}

View File

@ -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';

View File

@ -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"),

View File

@ -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(

View File

@ -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;
}
}

57
src/Radio/StereoTool.php Normal file
View File

@ -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;
}
}

View File

@ -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'),
];

View File

@ -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

View File

@ -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

View File

@ -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