#6771 -- Update high-performance now playing system.
This commit is contained in:
parent
38d9be3c65
commit
7ae8a48341
|
@ -87,6 +87,8 @@ jobs:
|
|||
path: vendor
|
||||
key: ${{ runner.OS }}-build-${{ hashFiles('composer.lock') }}
|
||||
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Set console permissions and clear static assets.
|
||||
run: |
|
||||
rm -rf web/static/vite_dist
|
||||
|
@ -100,7 +102,7 @@ jobs:
|
|||
- name: Generate new translations from existing code.
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci --include=dev
|
||||
bun install
|
||||
|
||||
cd ..
|
||||
bin/console locale:generate
|
||||
|
|
|
@ -14,6 +14,10 @@ release channel, you can take advantage of these new features and fixes.
|
|||
|
||||
## Code Quality/Technical Changes
|
||||
|
||||
- We are switching our implementation of High-Performance Now Playing updates to a simple script that we directly build
|
||||
and maintain. This will greatly simplify the process of using and updating our high-performance nowplaying data, and
|
||||
will also offer other improvements like sending Now Playing updates immediately upon initial connection.
|
||||
|
||||
- MariaDB has been updated to 11.2. Databases will automatically be upgraded on the first boot after updating.
|
||||
|
||||
- If you upload media to a folder and that folder is set to auto-assign to a playlist, the media will *instantly* be a
|
||||
|
|
15
Dockerfile
15
Dockerfile
|
@ -10,8 +10,6 @@ RUN go install github.com/jwilder/dockerize@v0.6.1
|
|||
|
||||
RUN go install github.com/aptible/supercronic@v0.2.28
|
||||
|
||||
RUN go install github.com/centrifugal/centrifugo/v5@v5.1.2
|
||||
|
||||
#
|
||||
# MariaDB dependencies build step
|
||||
#
|
||||
|
@ -27,7 +25,6 @@ ENV TZ="UTC"
|
|||
# Add Go dependencies
|
||||
COPY --from=go-dependencies /go/bin/dockerize /usr/local/bin
|
||||
COPY --from=go-dependencies /go/bin/supercronic /usr/local/bin/supercronic
|
||||
COPY --from=go-dependencies /go/bin/centrifugo /usr/local/bin/centrifugo
|
||||
|
||||
# Add MariaDB dependencies
|
||||
COPY --from=mariadb /usr/local/bin/healthcheck.sh /usr/local/bin/db_healthcheck.sh
|
||||
|
@ -103,7 +100,7 @@ RUN composer install --no-ansi --no-interaction
|
|||
|
||||
WORKDIR /var/azuracast/www/frontend
|
||||
|
||||
RUN npm ci --include=dev
|
||||
RUN bun install
|
||||
|
||||
WORKDIR /var/azuracast/www
|
||||
|
||||
|
@ -115,7 +112,7 @@ EXPOSE 8000-8999
|
|||
# Sensible default environment variables.
|
||||
ENV TZ="UTC" \
|
||||
LANG="en_US.UTF-8" \
|
||||
PATH="${PATH}:/var/azuracast/servers/shoutcast2" \
|
||||
PATH="${PATH}:/var/azuracast/storage/shoutcast2" \
|
||||
DOCKER_IS_STANDALONE="true" \
|
||||
APPLICATION_ENV="development" \
|
||||
MYSQL_HOST="localhost" \
|
||||
|
@ -163,6 +160,12 @@ COPY --chown=azuracast:azuracast . .
|
|||
|
||||
RUN composer dump-autoload --optimize --classmap-authoritative
|
||||
|
||||
WORKDIR /var/azuracast/www/frontend
|
||||
|
||||
RUN bun install --production
|
||||
|
||||
WORKDIR /var/azuracast/www
|
||||
|
||||
USER root
|
||||
|
||||
EXPOSE 80 443 2022
|
||||
|
@ -171,7 +174,7 @@ EXPOSE 8000-8999
|
|||
# Sensible default environment variables.
|
||||
ENV TZ="UTC" \
|
||||
LANG="en_US.UTF-8" \
|
||||
PATH="${PATH}:/var/azuracast/servers/shoutcast2" \
|
||||
PATH="${PATH}:/var/azuracast/storage/shoutcast2" \
|
||||
DOCKER_IS_STANDALONE="true" \
|
||||
APPLICATION_ENV="production" \
|
||||
MYSQL_HOST="localhost" \
|
||||
|
|
|
@ -130,7 +130,6 @@ return static function (CallableEventDispatcherInterface $dispatcher) {
|
|||
App\Sync\Task\RotateLogsTask::class,
|
||||
App\Sync\Task\RunAnalyticsTask::class,
|
||||
App\Sync\Task\RunBackupTask::class,
|
||||
App\Sync\Task\SendTimeOnSocketTask::class,
|
||||
App\Sync\Task\UpdateGeoLiteTask::class,
|
||||
App\Sync\Task\UpdateStorageLocationSizesTask::class,
|
||||
]);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -6,7 +6,9 @@
|
|||
"build": "vite build",
|
||||
"serve": "vite",
|
||||
"generate-locales": "vue-gettext-extract",
|
||||
"generate-api": "swagger-typescript-api --path http://localhost/api/openapi.yml --output ./src/entities --name ApiInterfaces.ts --no-client"
|
||||
"generate-api": "swagger-typescript-api --path http://localhost/api/openapi.yml --output ./src/entities --name ApiInterfaces.ts --no-client",
|
||||
"hpnp-dev": "bun --hot ./src/hpnp/index.ts",
|
||||
"hpnp-prod": "bun ./src/hpnp/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.0.1",
|
||||
|
@ -19,11 +21,13 @@
|
|||
"@fullcalendar/timegrid": "^6",
|
||||
"@fullcalendar/vue3": "^6",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@tinyhttp/app": "^2.2.1",
|
||||
"@vuelidate/core": "^2.0.0",
|
||||
"@vuelidate/validators": "^2.0.0",
|
||||
"@vuepic/vue-datepicker": "^7",
|
||||
"@vueuse/core": "^10",
|
||||
"axios": "^1",
|
||||
"better-sse": "^0.10.0",
|
||||
"bootstrap": "^5.3.0",
|
||||
"chart.js": "^4.2.1",
|
||||
"chartjs-adapter-luxon": "^1.1.0",
|
||||
|
@ -35,6 +39,7 @@
|
|||
"leaflet-fullscreen": "^1.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3",
|
||||
"milliparsec": "^2.3.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
|
|
|
@ -1,48 +1,50 @@
|
|||
import NowPlaying from '~/entities/NowPlaying';
|
||||
import {computed, onMounted, ref, shallowRef, watch} from "vue";
|
||||
import {computed, onMounted, Ref, ref, ShallowRef, shallowRef, watch} from "vue";
|
||||
import {useEventSource, useIntervalFn} from "@vueuse/core";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
import {has} from "lodash";
|
||||
import formatTime from "~/functions/formatTime";
|
||||
import {ApiNowPlaying} from "~/entities/ApiInterfaces.ts";
|
||||
import {getApiUrl} from "~/router.ts";
|
||||
import {useAxios} from "~/vendor/axios.ts";
|
||||
import formatTime from "~/functions/formatTime.ts";
|
||||
|
||||
export const nowPlayingProps = {
|
||||
nowPlayingUri: {
|
||||
stationShortName: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
useStatic: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
useSse: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
sseUri: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
initialNowPlaying: {
|
||||
type: Object,
|
||||
default() {
|
||||
return NowPlaying;
|
||||
}
|
||||
},
|
||||
timeUri: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
};
|
||||
|
||||
interface NowPlayingSSETime {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
interface NowPlayingSSEResponse {
|
||||
type: string,
|
||||
payload: NowPlayingSSETime | ApiNowPlaying
|
||||
}
|
||||
|
||||
export default function useNowPlaying(props) {
|
||||
const np = shallowRef(props.initialNowPlaying);
|
||||
const np: ShallowRef<ApiNowPlaying> = shallowRef(NowPlaying);
|
||||
const npUpdated: Ref<number> = ref(0);
|
||||
|
||||
const currentTime = ref(Math.floor(Date.now() / 1000));
|
||||
const currentTrackDuration = ref(0);
|
||||
const currentTrackElapsed = ref(0);
|
||||
const currentTime: Ref<number> = ref(Math.floor(Date.now() / 1000));
|
||||
const currentTrackDuration: Ref<number> = ref(0);
|
||||
const currentTrackElapsed: Ref<number> = ref(0);
|
||||
|
||||
const setNowPlaying = (np_new) => {
|
||||
const setNowPlaying = (np_new: ApiNowPlaying) => {
|
||||
np.value = np_new;
|
||||
npUpdated.value = currentTime.value;
|
||||
|
||||
currentTrackDuration.value = np_new?.now_playing?.duration ?? 0;
|
||||
currentTrackDuration.value = np_new.now_playing.duration ?? 0;
|
||||
|
||||
// Update the browser metadata for browsers that support it (i.e. Mobile Chrome)
|
||||
if ('mediaSession' in navigator) {
|
||||
|
@ -60,27 +62,35 @@ export default function useNowPlaying(props) {
|
|||
}));
|
||||
}
|
||||
|
||||
// Trigger initial NP set.
|
||||
setNowPlaying(np.value);
|
||||
|
||||
if (props.useSse) {
|
||||
const {data} = useEventSource(props.sseUri);
|
||||
watch(data, (data_raw) => {
|
||||
const json_data = JSON.parse(data_raw);
|
||||
const json_data_np = json_data?.pub?.data ?? {};
|
||||
const sseUri = getApiUrl(`/live/nowplaying/${props.stationShortName}`);
|
||||
|
||||
if (has(json_data_np, 'np')) {
|
||||
setTimeout(() => {
|
||||
setNowPlaying(json_data_np.np);
|
||||
}, 3000);
|
||||
} else if (has(json_data_np, 'time')) {
|
||||
currentTime.value = json_data_np.time;
|
||||
const {data} = useEventSource(sseUri.value);
|
||||
watch(data, (dataRaw: string) => {
|
||||
const jsonData: NowPlayingSSEResponse = JSON.parse(dataRaw);
|
||||
|
||||
if (jsonData.type === 'time') {
|
||||
currentTime.value = jsonData.payload.timestamp;
|
||||
} else if (jsonData.type === 'nowplaying') {
|
||||
if (npUpdated.value === 0) {
|
||||
setNowPlaying(jsonData.payload);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setNowPlaying(jsonData.payload);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const nowPlayingUri = props.useStatic
|
||||
? getApiUrl(`/nowplaying_static/${props.stationShortName}`)
|
||||
: getApiUrl(`/nowplaying/${props.stationShortName}`);
|
||||
|
||||
const timeUri = getApiUrl('/time');
|
||||
|
||||
const {axios} = useAxios();
|
||||
const checkNowPlaying = () => {
|
||||
axios.get(props.nowPlayingUri, {
|
||||
axios.get(nowPlayingUri.value, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
|
@ -96,7 +106,7 @@ export default function useNowPlaying(props) {
|
|||
};
|
||||
|
||||
const checkTime = () => {
|
||||
axios.get(props.timeUri, {
|
||||
axios.get(timeUri.value, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
|
@ -110,8 +120,8 @@ export default function useNowPlaying(props) {
|
|||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(checkTime, 5000);
|
||||
setTimeout(checkNowPlaying, 5000);
|
||||
checkTime();
|
||||
checkNowPlaying();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -135,39 +145,30 @@ export default function useNowPlaying(props) {
|
|||
});
|
||||
|
||||
const currentTrackPercent = computed(() => {
|
||||
const $currentTrackElapsed = currentTrackElapsed.value;
|
||||
const $currentTrackDuration = currentTrackDuration.value;
|
||||
|
||||
if (!$currentTrackDuration) {
|
||||
if (!currentTrackDuration.value) {
|
||||
return 0;
|
||||
}
|
||||
if ($currentTrackElapsed > $currentTrackDuration) {
|
||||
if (currentTrackElapsed.value > currentTrackDuration.value) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return ($currentTrackElapsed / $currentTrackDuration) * 100;
|
||||
return (currentTrackElapsed.value / currentTrackDuration.value) * 100;
|
||||
});
|
||||
|
||||
const currentTrackDurationDisplay = computed(() => {
|
||||
const $currentTrackDuration = currentTrackDuration.value;
|
||||
return ($currentTrackDuration) ? formatTime($currentTrackDuration) : null;
|
||||
return (currentTrackDuration.value) ? formatTime(currentTrackDuration.value) : null;
|
||||
});
|
||||
|
||||
const currentTrackElapsedDisplay = computed(() => {
|
||||
let $currentTrackElapsed = currentTrackElapsed.value;
|
||||
const $currentTrackDuration = currentTrackDuration.value;
|
||||
|
||||
if (!$currentTrackDuration) {
|
||||
if (!currentTrackDuration.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($currentTrackElapsed > $currentTrackDuration) {
|
||||
$currentTrackElapsed = $currentTrackDuration;
|
||||
}
|
||||
|
||||
return formatTime($currentTrackElapsed);
|
||||
return (currentTrackElapsed.value <= currentTrackDuration.value)
|
||||
? formatTime(currentTrackElapsed.value)
|
||||
: currentTrackDurationDisplay.value;
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
np,
|
||||
currentTime,
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
import {ApiNowPlaying} from "~/entities/ApiInterfaces.ts";
|
||||
import {Channel, createChannel, createSession, Session} from "better-sse";
|
||||
import {App} from '@tinyhttp/app';
|
||||
import {json} from "milliparsec";
|
||||
|
||||
const publicPort: number = 6050;
|
||||
const internalPort: number = 6055;
|
||||
|
||||
interface NowPlayingSubmission {
|
||||
channel: string,
|
||||
payload: ApiNowPlaying
|
||||
}
|
||||
|
||||
interface StationChannelState extends Record<string, unknown> {
|
||||
timestamp: number,
|
||||
lastMessage: ApiNowPlaying
|
||||
}
|
||||
|
||||
const unixTimestamp = (): number => Math.floor(Date.now() / 1000);
|
||||
|
||||
const timeChannel = createChannel();
|
||||
timeChannel.on("session-registered", (session: Session) => {
|
||||
session.push({
|
||||
type: 'time',
|
||||
payload: {
|
||||
timestamp: unixTimestamp()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const stationChannels: Map<string, Channel<StationChannelState>> = new Map();
|
||||
|
||||
// Routine time ping.
|
||||
setInterval(() => {
|
||||
console.debug('Sending time ping...');
|
||||
timeChannel.broadcast({
|
||||
type: 'time',
|
||||
payload: {
|
||||
timestamp: unixTimestamp()
|
||||
}
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
// If a station hasn't posted NP updates in a specified time, close its channel and garbage-collect its sessions.
|
||||
setInterval(() => {
|
||||
const threshold = unixTimestamp() - 120;
|
||||
|
||||
for (const [key, channel] of stationChannels) {
|
||||
if (channel.state.timestamp < threshold) {
|
||||
channel.activeSessions.forEach((session) => {
|
||||
channel.deregister(session);
|
||||
});
|
||||
stationChannels.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
const publicServer = new App();
|
||||
|
||||
publicServer.get('/:station', async (req, res) => {
|
||||
const stationId: string = req.params.station;
|
||||
|
||||
if (!stationChannels.has(stationId)) {
|
||||
res.status(404).send('Station Not Found');
|
||||
}
|
||||
|
||||
const session = await createSession(req, res, {
|
||||
retry: 5000,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-Accel-Buffering": "no",
|
||||
}
|
||||
});
|
||||
|
||||
timeChannel.register(session);
|
||||
|
||||
const stationChannel = stationChannels.get(stationId);
|
||||
stationChannel.register(session);
|
||||
});
|
||||
|
||||
publicServer.listen(publicPort, () => {
|
||||
console.debug(`Public server listening on port ${publicPort}...`);
|
||||
});
|
||||
|
||||
const privateServer = new App();
|
||||
|
||||
privateServer.use(json());
|
||||
|
||||
privateServer.post('/', async (req, res) => {
|
||||
const body: NowPlayingSubmission = req.body;
|
||||
|
||||
console.debug(
|
||||
`NP Update received for channel ${body.channel}.`
|
||||
);
|
||||
|
||||
let channel: Channel<StationChannelState>;
|
||||
if (stationChannels.has(body.channel)) {
|
||||
channel = stationChannels.get(body.channel);
|
||||
} else {
|
||||
// Create a new channel if none exists.
|
||||
channel = createChannel();
|
||||
channel.on("session-registered", (session: Session) => {
|
||||
session.push({
|
||||
type: 'nowplaying',
|
||||
payload: channel.state.lastMessage
|
||||
});
|
||||
});
|
||||
stationChannels.set(body.channel, channel);
|
||||
}
|
||||
|
||||
channel.state.timestamp = unixTimestamp();
|
||||
channel.state.lastMessage = body.payload;
|
||||
channel.broadcast({
|
||||
type: 'nowplaying',
|
||||
payload: body.payload
|
||||
});
|
||||
|
||||
return res.send('OK');
|
||||
});
|
||||
|
||||
privateServer.listen(internalPort, () => {
|
||||
console.debug(`Internal server listening on port ${internalPort}...`);
|
||||
});
|
|
@ -8,12 +8,13 @@ use App\Container\EnvironmentAwareTrait;
|
|||
use App\Entity\Station;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
final class Centrifugo
|
||||
/**
|
||||
* Utility class for the High-Performance Now-Playing (HPNP) library.
|
||||
*/
|
||||
final class HpNp
|
||||
{
|
||||
use EnvironmentAwareTrait;
|
||||
|
||||
public const GLOBAL_TIME_CHANNEL = 'global:time';
|
||||
|
||||
public function __construct(
|
||||
private readonly Client $client,
|
||||
) {
|
||||
|
@ -24,38 +25,15 @@ final class Centrifugo
|
|||
return $this->environment->isDocker() && !$this->environment->isTesting();
|
||||
}
|
||||
|
||||
public function sendTime(): void
|
||||
{
|
||||
$this->send([
|
||||
'method' => 'publish',
|
||||
'params' => [
|
||||
'channel' => self::GLOBAL_TIME_CHANNEL,
|
||||
'data' => [
|
||||
'time' => time(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function publishToStation(Station $station, mixed $message): void
|
||||
{
|
||||
$this->send([
|
||||
'method' => 'publish',
|
||||
'params' => [
|
||||
'channel' => $this->getChannelName($station),
|
||||
'data' => [
|
||||
'np' => $message,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function send(array $body): void
|
||||
{
|
||||
$this->client->post(
|
||||
'http://localhost:6025/api',
|
||||
'http://localhost:6055',
|
||||
[
|
||||
'json' => $body,
|
||||
'json' => [
|
||||
'channel' => $station->getShortName(),
|
||||
'payload' => $message,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -76,9 +54,4 @@ final class Centrifugo
|
|||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getChannelName(Station $station): string
|
||||
{
|
||||
return 'station:' . $station->getShortName();
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ final class ServiceControl
|
|||
|
||||
public function __construct(
|
||||
private readonly SupervisorInterface $supervisor,
|
||||
private readonly Centrifugo $centrifugo
|
||||
private readonly HpNp $hpNp
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -85,12 +85,12 @@ final class ServiceControl
|
|||
'php-worker' => __('PHP queue processing worker'),
|
||||
'redis' => __('Cache'),
|
||||
'sftpgo' => __('SFTP service'),
|
||||
'centrifugo' => __('Live Now Playing updates'),
|
||||
'hpnp' => __('High-Performance Now Playing updates'),
|
||||
'vite' => __('Frontend Assets'),
|
||||
];
|
||||
|
||||
if (!$this->centrifugo->isSupported()) {
|
||||
unset($services['centrifugo']);
|
||||
if (!$this->hpNp->isSupported()) {
|
||||
unset($services['hpnp']);
|
||||
}
|
||||
|
||||
if (!$this->environment->useLocalDatabase()) {
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Sync\Task;
|
||||
|
||||
use App\Service\Centrifugo;
|
||||
|
||||
final class SendTimeOnSocketTask extends AbstractTask
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Centrifugo $centrifugo,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSchedulePattern(): string
|
||||
{
|
||||
return self::SCHEDULE_EVERY_MINUTE;
|
||||
}
|
||||
|
||||
public function run(bool $force = false): void
|
||||
{
|
||||
if (!$this->centrifugo->isSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->centrifugo->sendTime();
|
||||
}
|
||||
}
|
|
@ -4,29 +4,21 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\VueComponent;
|
||||
|
||||
use App\Entity\ApiGenerator\NowPlayingApiGenerator;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Service\Centrifugo;
|
||||
use App\Service\HpNp;
|
||||
|
||||
final class NowPlayingComponent implements VueComponentInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NowPlayingApiGenerator $npApiGenerator,
|
||||
private readonly Centrifugo $centrifugo
|
||||
private readonly HpNp $hpNp
|
||||
) {
|
||||
}
|
||||
|
||||
public function getProps(ServerRequest $request): array
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
$baseUrl = $request->getRouter()->getBaseUrl();
|
||||
|
||||
$np = $this->npApiGenerator->currentOrEmpty($station);
|
||||
$np->resolveUrls($baseUrl);
|
||||
|
||||
$customization = $request->getCustomization();
|
||||
|
||||
$station = $request->getStation();
|
||||
$backendConfig = $station->getBackendConfig();
|
||||
|
||||
return [
|
||||
|
@ -41,29 +33,12 @@ final class NowPlayingComponent implements VueComponentInterface
|
|||
public function getDataProps(ServerRequest $request): array
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
$baseUrl = $request->getRouter()->getBaseUrl();
|
||||
|
||||
$np = $this->npApiGenerator->currentOrEmpty($station);
|
||||
$np->resolveUrls($baseUrl);
|
||||
|
||||
$customization = $request->getCustomization();
|
||||
$router = $request->getRouter();
|
||||
|
||||
$props = [
|
||||
'initialNowPlaying' => $np,
|
||||
'nowPlayingUri' => $customization->useStaticNowPlaying()
|
||||
? '/api/nowplaying_static/' . urlencode($station->getShortName()) . '.json'
|
||||
: $router->named('api:nowplaying:index', ['station_id' => $station->getShortName()]),
|
||||
'timeUri' => $router->named('api:index:time'),
|
||||
'useSse' => false,
|
||||
return [
|
||||
'stationShortName' => $station->getShortName(),
|
||||
'useStatic' => $customization->useStaticNowPlaying(),
|
||||
'useSse' => $customization->useStaticNowPlaying() && $this->hpNp->isSupported(),
|
||||
];
|
||||
|
||||
if ($customization->useStaticNowPlaying() && $this->centrifugo->isSupported()) {
|
||||
$props['useSse'] = true;
|
||||
$props['sseUri'] = $this->centrifugo->getSseUrl($station);
|
||||
}
|
||||
|
||||
return $props;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use App\Container\EnvironmentAwareTrait;
|
|||
use App\Container\LoggerAwareTrait;
|
||||
use App\Entity\Api\NowPlaying\NowPlaying;
|
||||
use App\Entity\Station;
|
||||
use App\Service\Centrifugo;
|
||||
use App\Service\HpNp;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
use const JSON_PRETTY_PRINT;
|
||||
|
@ -21,7 +21,7 @@ final class LocalWebhookHandler
|
|||
public const NAME = 'local';
|
||||
|
||||
public function __construct(
|
||||
private readonly Centrifugo $centrifugo
|
||||
private readonly HpNp $hpNp
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -68,8 +68,8 @@ final class LocalWebhookHandler
|
|||
);
|
||||
|
||||
// Publish to websocket library
|
||||
if ($this->centrifugo->isSupported()) {
|
||||
$this->centrifugo->publishToStation($station, $np);
|
||||
if ($this->hpNp->isSupported()) {
|
||||
$this->hpNp->publishToStation($station, $np);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,3 +7,5 @@ apt-get -y autoremove
|
|||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
rm -rf /tmp/tmp*
|
||||
|
||||
chmod -R a+x /usr/local/bin
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[program:centrifugo]
|
||||
directory=/var/azuracast/centrifugo
|
||||
command=centrifugo -c /var/azuracast/centrifugo/config.yaml
|
||||
[program:hpnp]
|
||||
directory=/var/azuracast/www/frontend
|
||||
command=bun run hpnp-dev
|
||||
user=azuracast
|
||||
priority=700
|
||||
numprocs=1
|
||||
|
@ -10,7 +10,7 @@ autorestart=true
|
|||
stopasgroup=true
|
||||
killasgroup=true
|
||||
|
||||
stdout_logfile=/var/azuracast/www_tmp/service_centrifugo.log
|
||||
stdout_logfile=/var/azuracast/www_tmp/service_hpnp.log
|
||||
stdout_logfile_maxbytes=5MB
|
||||
stdout_logfile_backups=5
|
||||
redirect_stderr=true
|
|
@ -1,34 +0,0 @@
|
|||
allow_anonymous_connect_without_token: true
|
||||
api_insecure: true
|
||||
admin: true
|
||||
admin_insecure: true
|
||||
port: 6020
|
||||
internal_port: 6025
|
||||
websocket_disable: true
|
||||
uni_websocket: true
|
||||
uni_sse: true
|
||||
uni_http_stream: true
|
||||
allowed_origins:
|
||||
- "*"
|
||||
|
||||
namespaces:
|
||||
- name: "station"
|
||||
history_size: 1
|
||||
history_ttl: "30s"
|
||||
allow_subscribe_for_client: true
|
||||
allow_subscribe_for_anonymous: true
|
||||
allow_history_for_client: true
|
||||
allow_history_for_anonymous: true
|
||||
|
||||
- name: "global"
|
||||
history_size: 0
|
||||
allow_subscribe_for_client: true
|
||||
allow_subscribe_for_anonymous: true
|
||||
allow_history_for_client: true
|
||||
allow_history_for_anonymous: true
|
||||
|
||||
{{if isTrue .Env.ENABLE_REDIS }}
|
||||
engine: "redis"
|
||||
redis_address: "{{ .Env.REDIS_HOST }}:{{ default .Env.REDIS_PORT "6379" }}"
|
||||
redis_db: 0
|
||||
{{end}}
|
|
@ -6,8 +6,8 @@ upstream php-fpm-www {
|
|||
server unix:/var/run/php-fpm-www.sock;
|
||||
}
|
||||
|
||||
upstream centrifugo {
|
||||
server 127.0.0.1:6020;
|
||||
upstream hpnp {
|
||||
server 127.0.0.1:6050;
|
||||
}
|
||||
|
||||
{{if eq .Env.APPLICATION_ENV "development"}}
|
||||
|
@ -95,10 +95,10 @@ server {
|
|||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Websocket/SSE Now Playing Updates
|
||||
# SSE Now Playing Updates
|
||||
location ~ ^/api/live/nowplaying/(\w+)$ {
|
||||
include proxy_params;
|
||||
proxy_pass http://centrifugo/connection/uni_$1?$args;
|
||||
proxy_pass http://hpnp/$1?$args;
|
||||
}
|
||||
|
||||
# Default clean URL routing
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[program:hpnp]
|
||||
directory=/var/azuracast/www/frontend
|
||||
command=bun run hpnp-prod
|
||||
user=azuracast
|
||||
priority=700
|
||||
numprocs=1
|
||||
autostart=true
|
||||
autorestart=true
|
||||
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
|
||||
stdout_logfile=/var/azuracast/www_tmp/service_hpnp.log
|
||||
stdout_logfile_maxbytes=5MB
|
||||
stdout_logfile_backups=5
|
||||
redirect_stderr=true
|
||||
|
||||
stdout_events_enabled = true
|
||||
stderr_events_enabled = true
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
curl -fsSL https://bun.sh/install | gosu azuracast bash
|
||||
|
||||
ln -s /var/azuracast/.bun/bin/bun /usr/local/bin/bun
|
||||
ln -s /var/azuracast/.bun/bin/bunx /usr/local/bin/bunx
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
mkdir -p /var/azuracast/centrifugo
|
||||
cp /bd_build/web/centrifugo/config.yaml.tmpl /var/azuracast/centrifugo/config.yaml.tmpl
|
||||
|
Loading…
Reference in New Issue