Support custom "Station Offline" and "Live Broadcast" text per-station.

This commit is contained in:
Buster Neece 2023-08-11 14:20:59 -05:00
parent fccf41032c
commit 6b3d2b2095
No known key found for this signature in database
16 changed files with 103 additions and 20 deletions

View File

@ -24,6 +24,11 @@ release channel, you can take advantage of these new features and fixes.
tasks, with a secondary instance that will automatically take over these functions if the main installation is
nonresponsive.
- **Custom "Station Offline" and "Live Broadcast" Messages**: You can now specify, on a per-station level, the default
message that shows when the station is offline or when a DJ has connected but has not yet sent any metadata. The
former is located on the Branding Configuration page, and the latter is under the "Streamers/DJs" tab on the station
profile.
## Code Quality/Technical Changes
- **Frontend Overhaul**: We have updated the code that powers the browser-facing frontend of our application. In

View File

@ -74,6 +74,6 @@ return static function (RouteCollectorProxy $app) {
->setName('public:podcast:feed');
}
)
->add(Middleware\GetStation::class)
->add(Middleware\EnableView::class);
->add(Middleware\EnableView::class)
->add(Middleware\GetStation::class);
};

View File

@ -112,6 +112,14 @@
:label="$gettext('Customize DJ/Streamer Mount Point')"
:description="$gettext('If your streaming software requires a specific mount point path, specify it here. Otherwise, use the default.')"
/>
<form-group-field
id="edit_form_backend_live_broadcast_text"
class="col-md-6"
:field="v$.backend_config.live_broadcast_text"
:label="$gettext('Default Live Broadcast Message')"
:description="$gettext('If a live DJ connects but has not yet sent metadata, this is the message that will display on player pages.')"
/>
</div>
</template>
</form-fieldset>
@ -148,6 +156,7 @@ const {enableAdvancedFeatures} = useAzuraCast();
const emit = defineEmits(['update:form']);
const form = useVModel(props, 'form', emit);
const {v$, tabClass} = useVuelidateOnFormTab(
computed(() => {
let validations = {
@ -158,6 +167,7 @@ const {v$, tabClass} = useVuelidateOnFormTab(
record_streams_format: {},
record_streams_bitrate: {},
dj_buffer: {numeric},
live_broadcast_text: {}
}
};
@ -184,6 +194,7 @@ const {v$, tabClass} = useVuelidateOnFormTab(
record_streams_format: 'mp3',
record_streams_bitrate: 128,
dj_buffer: 5,
live_broadcast_text: 'Live Broadcast'
}
};

View File

@ -27,7 +27,7 @@
<div v-if="!np.is_online">
<h4 class="now-playing-title text-muted">
{{ $gettext('Station Offline') }}
{{ offlineText ?? $gettext('Station Offline') }}
</h4>
</div>
<div v-else-if="np.now_playing.song.title !== ''">
@ -148,6 +148,7 @@ import useNowPlaying from "~/functions/useNowPlaying";
import playerProps from "~/components/Public/playerProps";
import MuteButton from "~/components/Common/MuteButton.vue";
import AlbumArt from "~/components/Common/AlbumArt.vue";
import {useAzuraCastStation} from "~/vendor/azuracast";
const props = defineProps({
...playerProps
@ -155,6 +156,8 @@ const props = defineProps({
const emit = defineEmits(['np_updated']);
const {offlineText} = useAzuraCastStation();
const {
np,
currentTrackPercent,

View File

@ -23,6 +23,14 @@
<loading :loading="isLoading">
<div class="card-body">
<div class="row g-3">
<form-group-field
id="form_edit_offline_text"
class="col-md-6"
:field="v$.offline_text"
:label="$gettext('Station Offline Display Text')"
:description="$gettext('This will be shown on public player pages if the station is offline. Leave blank to default to a localized version of &quot;%{message}&quot;.', {message: $gettext('Station Offline')})"
/>
<form-group-field
id="form_edit_default_album_art_url"
class="col-md-6"
@ -99,14 +107,16 @@ const error = ref(null);
const {form, resetForm, v$, ifValid} = useVuelidateOnForm(
{
'default_album_art_url': {},
'public_custom_css': {},
'public_custom_js': {},
default_album_art_url: {},
public_custom_css: {},
public_custom_js: {},
offline_text: {}
},
{
'default_album_art_url': '',
'public_custom_css': '',
'public_custom_js': ''
default_album_art_url: '',
public_custom_css: '',
public_custom_js: '',
offline_text: ''
}
);

View File

@ -76,7 +76,7 @@
<div class="d-table-cell align-middle w-100">
<div v-if="!np.is_online">
<h5 class="media-heading m-0 text-muted">
{{ $gettext('Station Offline') }}
{{ offlineText ?? $gettext('Station Offline') }}
</h5>
</div>
<div v-else-if="np.now_playing.song.title !== ''">
@ -238,6 +238,7 @@ import Loading from "~/components/Common/Loading.vue";
import CardPage from "~/components/Common/CardPage.vue";
import {useLightbox} from "~/vendor/lightbox";
import {StationPermission, userAllowedForStation} from "~/acl";
import {useAzuraCastStation} from "~/vendor/azuracast";
const props = defineProps({
...nowPlayingPanelProps,
@ -245,6 +246,8 @@ const props = defineProps({
const emit = defineEmits(['api-call']);
const {offlineText} = useAzuraCastStation();
const {
np,
currentTrackDurationDisplay,

View File

@ -13,6 +13,7 @@ export interface AzuraCastStationConstants {
name: string | null,
shortName: string | null,
timezone: string | null,
offlineText: string | null,
}
export interface AzuraCastUserConstants {
@ -60,6 +61,7 @@ export function useAzuraCastStation(): AzuraCastStationConstants {
id: null,
name: null,
shortName: null,
timezone: null
timezone: null,
offlineText: null
};
}

View File

@ -101,7 +101,7 @@ final class SoundExchangeAction implements SingleActionInterface
$historyRowsById = array_column($historyRows, null, 'media_id');
// Remove any reference to the "Stream Offline" song.
$offlineSongHash = Song::createOffline()->getSongId();
$offlineSongHash = Song::OFFLINE_SONG_ID;
unset($historyRowsById[$offlineSongHash]);
// Assemble report items

View File

@ -164,7 +164,7 @@ final class NowPlayingApiGenerator
$np->station = $this->stationApiGenerator->__invoke($station, $baseUri);
$np->listeners = new Listeners();
$songObj = Song::createOffline();
$songObj = Song::createOffline($station->getBrandingConfig()->getOfflineText());
$offlineApiNowPlaying = new CurrentSong();
$offlineApiNowPlaying->sh_id = 0;

View File

@ -12,6 +12,8 @@ class Song implements SongInterface
{
use Traits\HasSongFields;
public final const OFFLINE_SONG_ID = '5a6a865199cf5df73b1417326d2ff24f';
public function __construct(?SongInterface $song = null)
{
if (null !== $song) {
@ -62,6 +64,10 @@ class Song implements SongInterface
$songText = str_replace($removeChars, '', $songText);
if (empty($songText)) {
return self::OFFLINE_SONG_ID;
}
$hashBase = mb_strtolower($songText, 'UTF-8');
return md5($hashBase);
}
@ -104,8 +110,10 @@ class Song implements SongInterface
return self::createFromNowPlayingSong($currentSong);
}
public static function createOffline(): self
public static function createOffline(?string $text = null): self
{
return self::createFromText('Station Offline');
$song = self::createFromText($text ?? 'Station Offline');
$song->setSongId(self::OFFLINE_SONG_ID);
return $song;
}
}

View File

@ -421,6 +421,22 @@ class StationBackendConfiguration extends AbstractStationConfiguration
$this->set(self::HLS_IS_DEFAULT, $value);
}
public const LIVE_BROADCAST_TEXT = 'live_broadcast_text';
public function getLiveBroadcastText(): string
{
$text = $this->get(self::LIVE_BROADCAST_TEXT);
return (!empty($text))
? $text
: 'Live Broadcast';
}
public function setLiveBroadcastText(?string $text): void
{
$this->set(self::LIVE_BROADCAST_TEXT, $text);
}
public const CUSTOM_TOP = 'custom_config_top';
public const CUSTOM_PRE_PLAYLISTS = 'custom_config_pre_playlists';
public const CUSTOM_PRE_LIVE = 'custom_config_pre_live';

View File

@ -53,4 +53,20 @@ class StationBrandingConfiguration extends AbstractStationConfiguration
{
$this->set(self::PUBLIC_CUSTOM_JS, $js);
}
public const OFFLINE_TEXT = 'offline_text';
public function getOfflineText(): ?string
{
$message = $this->get(self::OFFLINE_TEXT);
return (!empty($message))
? $message
: null;
}
public function setOfflineText(?string $message): void
{
$this->set(self::OFFLINE_TEXT, $message);
}
}

View File

@ -60,12 +60,17 @@ trait HasSongFields
return $this->song_id;
}
protected function setSongId(string $songId): void
{
$this->song_id = $songId;
}
public function updateSongId(): void
{
$text = $this->getText();
$this->song_id = (null !== $text)
$this->song_id = !empty($text)
? Song::getSongHash($text)
: Song::createOffline()->getSongId();
: Song::OFFLINE_SONG_ID;
}
public function getText(): ?string

View File

@ -67,8 +67,7 @@ final class RemoteAlbumArt
public function getUrlForSong(SongInterface $song): ?string
{
// Avoid tracks that shouldn't ever hit remote APIs.
$offlineSong = Song::createOffline();
if ($song->getSongId() === $offlineSong->getSongId()) {
if ($song->getSongId() === Song::OFFLINE_SONG_ID) {
return null;
}

View File

@ -1010,6 +1010,10 @@ final class ConfigWriter implements EventSubscriberInterface
$harborParams = implode(', ', $harborParams);
$liveBroadcastText = self::cleanUpString(
$settings->getLiveBroadcastText()
);
$event->appendBlock(
<<<LIQ
# A Pre-DJ source of radio that can be broadcast if needed
@ -1021,7 +1025,7 @@ final class ConfigWriter implements EventSubscriberInterface
def insert_missing(m) =
if m == [] then
[("title", "Live Broadcast"), ("is_live", "true")]
[("title", "{$liveBroadcastText}"), ("is_live", "true")]
else
[("is_live", "true")]
end

View File

@ -229,6 +229,7 @@ final class View extends Engine
'name' => $station->getName(),
'shortName' => $station->getShortName(),
'timezone' => $station->getTimezone(),
'offlineText' => $station->getBrandingConfig()->getOfflineText(),
]);
}