Use common NowPlaying component for station profile.

This commit is contained in:
Buster Neece 2023-01-09 17:03:03 -06:00
parent 6e96d11804
commit ff2402c556
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
11 changed files with 124 additions and 176 deletions

View File

@ -230,11 +230,11 @@ import AccountEditModal from "./Account/EditModal";
import Avatar from "~/components/Common/Avatar";
import InfoCard from "~/components/Common/InfoCard";
import EnabledBadge from "~/components/Common/Badges/EnabledBadge.vue";
import {onMounted, ref} from "vue";
import {ref} from "vue";
import {useTranslate} from "~/vendor/gettext";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
import {useSweetAlert} from "~/vendor/sweetalert";
import useConfirmAndDelete from "~/functions/useConfirmAndDelete";
import useRefreshableAsyncState from "~/functions/useRefreshableAsyncState";
const props = defineProps({
userUrl: {
@ -261,22 +261,32 @@ const props = defineProps({
}
});
const userLoading = ref(true);
const user = ref({
name: null,
email: null,
avatar: {
url: null,
service: null,
serviceUrl: null
},
roles: [],
});
const {axios} = useAxios();
const securityLoading = ref(true);
const security = ref({
twoFactorEnabled: false,
});
const {state: user, isLoading: userLoading, execute: reloadUser} = useRefreshableAsyncState(
() => axios.get(props.userUrl).then((r) => r.data),
{
name: null,
email: null,
avatar: {
url: null,
service: null,
serviceUrl: null
},
roles: [],
},
);
const {state: security, isLoading: securityLoading, execute: reloadSecurity} = useRefreshableAsyncState(
() => axios.get(props.twoFactorUrl).then((r) => {
return {
twoFactorEnabled: r.data.two_factor_enabled
};
}),
{
twoFactorEnabled: false,
},
);
const {$gettext} = useTranslate();
@ -296,42 +306,13 @@ const apiKeyFields = [
];
const $dataTable = ref(); // DataTable
const {wrapWithLoading, notifySuccess} = useNotify();
const {axios} = useAxios();
const relist = () => {
userLoading.value = true;
wrapWithLoading(
axios.get(props.userUrl)
).then((resp) => {
user.value = {
name: resp.data.name,
email: resp.data.email,
roles: resp.data.roles,
avatar: {
url: resp.data.avatar.url_64,
service: resp.data.avatar.service_name,
serviceUrl: resp.data.avatar.service_url
}
};
userLoading.value = false;
});
securityLoading.value = true;
wrapWithLoading(
axios.get(props.twoFactorUrl)
).then((resp) => {
security.value.twoFactorEnabled = resp.data.two_factor_enabled;
securityLoading.value = false;
});
reloadUser();
reloadSecurity();
$dataTable.value?.relist();
};
onMounted(relist);
const reload = () => {
location.reload();
};
@ -354,22 +335,11 @@ const enableTwoFactor = () => {
$twoFactorModal.value?.open();
};
const {confirmDelete} = useSweetAlert();
const disableTwoFactor = () => {
confirmDelete({
title: $gettext('Disable two-factor authentication?'),
}).then((result) => {
if (result.value) {
wrapWithLoading(
axios.delete(props.twoFactorUrl)
).then((resp) => {
notifySuccess(resp.data.message);
relist();
});
}
});
};
const {doDelete: doDisableTwoFactor} = useConfirmAndDelete(
$gettext('Disable two-factor authentication?'),
relist
);
const disableTwoFactor = () => doDisableTwoFactor(props.twoFactorUrl);
const $apiKeyModal = ref(); // ApiKeyModal
@ -377,18 +347,8 @@ const createApiKey = () => {
$apiKeyModal.value?.create();
};
const deleteApiKey = (url) => {
confirmDelete({
title: $gettext('Delete API Key?'),
}).then((result) => {
if (result.value) {
wrapWithLoading(
axios.delete(url)
).then((resp) => {
notifySuccess(resp.data.message);
relist();
});
}
});
};
const {doDelete: deleteApiKey} = useConfirmAndDelete(
$gettext('Delete API Key?'),
relist
);
</script>

View File

@ -1,7 +1,7 @@
<template>
<profile-header
v-bind="pickProps(props, headerPanelProps)"
:np="np"
:station="profileInfo.station"
/>
<div
@ -11,16 +11,15 @@
<div class="col-lg-7">
<profile-now-playing
v-bind="pickProps(props, nowPlayingPanelProps)"
:np="np"
/>
<profile-schedule
:station-time-zone="stationTimeZone"
:schedule-items="np.schedule"
:schedule-items="profileInfo.schedule"
/>
<profile-streams
:np="np"
:station="profileInfo.station"
/>
<profile-public-pages
@ -42,14 +41,14 @@
<template v-if="hasActiveFrontend">
<profile-frontend
v-bind="pickProps(props, frontendPanelProps)"
:np="np"
:frontend-running="profileInfo.services.frontend_running"
/>
</template>
<template v-if="hasActiveBackend">
<profile-backend
v-bind="pickProps(props, backendPanelProps)"
:np="np"
:backend-running="profileInfo.services.backend_running"
/>
</template>
<template v-else>
@ -72,7 +71,7 @@ import ProfileBackendNone from './Profile/BackendNonePanel';
import ProfileBackend from './Profile/BackendPanel';
import {BACKEND_NONE, FRONTEND_REMOTE} from '~/components/Entity/RadioAdapters';
import NowPlaying from '~/components/Entity/NowPlaying';
import {computed, onMounted, shallowRef} from "vue";
import {computed} from "vue";
import {useAxios} from "~/vendor/axios";
import backendPanelProps from "./Profile/backendPanelProps";
import embedModalProps from "./Profile/embedModalProps";
@ -83,6 +82,8 @@ import publicPagesPanelProps from "./Profile/publicPagesPanelProps";
import requestsPanelProps from "./Profile/requestsPanelProps";
import streamersPanelProps from "./Profile/streamersPanelProps";
import {pickProps} from "~/functions/pickProps";
import useRefreshableAsyncState from "~/functions/useRefreshableAsyncState";
import {useIntervalFn} from "@vueuse/core";
const props = defineProps({
...backendPanelProps,
@ -111,16 +112,6 @@ const props = defineProps({
}
});
const np = shallowRef({
...NowPlaying,
loading: true,
services: {
backend_running: false,
frontend_running: false
},
schedule: []
});
const hasActiveFrontend = computed(() => {
return props.frontendType !== FRONTEND_REMOTE;
});
@ -131,20 +122,28 @@ const hasActiveBackend = computed(() => {
const {axios} = useAxios();
const checkNowPlaying = () => {
axios.get(props.profileApiUri).then((response) => {
let np_new = response.data;
np_new.loading = false;
const {state: profileInfo, execute: reloadProfile} = useRefreshableAsyncState(
() => axios.get(props.profileApiUri).then((r) => r.data),
{
station: {
...NowPlaying.station
},
services: {
backend_running: false,
frontend_running: false,
has_started: false,
needs_restart: false
},
schedule: []
}
);
np.value = np_new;
const profileReloadTimeout = computed(() => {
return (!document.hidden) ? 15000 : 30000
});
setTimeout(checkNowPlaying, (!document.hidden) ? 15000 : 30000);
}).catch((error) => {
if (!error.response || error.response.data.code !== 403) {
setTimeout(checkNowPlaying, (!document.hidden) ? 30000 : 120000);
}
});
}
onMounted(checkNowPlaying);
useIntervalFn(
reloadProfile,
profileReloadTimeout
);
</script>

View File

@ -7,7 +7,7 @@
<div class="card-header bg-primary-dark">
<h3 class="card-title">
{{ $gettext('AutoDJ Service') }}
<running-badge :running="np.services.backend_running" />
<running-badge :running="backendRunning" />
<br>
<small>{{ backendName }}</small>
</h3>
@ -43,7 +43,7 @@
{{ $gettext('Restart') }}
</a>
<a
v-show="!np.services.backend_running"
v-show="!backendRunning"
class="api-call no-reload btn btn-outline-success"
:href="backendStartUri"
>
@ -51,7 +51,7 @@
{{ $gettext('Start') }}
</a>
<a
v-show="np.services.backend_running"
v-show="backendRunning"
class="api-call no-reload btn btn-outline-danger"
:href="backendStopUri"
>
@ -72,8 +72,8 @@ import backendPanelProps from "~/components/Stations/Profile/backendPanelProps";
const props = defineProps({
...backendPanelProps,
np: {
type: Object,
backendRunning: {
type: Boolean,
required: true
}
});

View File

@ -8,7 +8,7 @@
<h3 class="card-title">
{{ $gettext('Broadcasting Service') }}
<running-badge :running="np.services.frontend_running" />
<running-badge :running="frontendRunning" />
<br>
<small>{{ frontendName }}</small>
</h3>
@ -103,7 +103,7 @@
{{ $gettext('Restart') }}
</a>
<a
v-show="!np.services.frontend_running"
v-show="!frontendRunning"
class="api-call no-reload btn btn-outline-success"
:href="frontendStartUri"
>
@ -111,7 +111,7 @@
{{ $gettext('Start') }}
</a>
<a
v-show="np.services.frontend_running"
v-show="frontendRunning"
class="api-call no-reload btn btn-outline-danger"
:href="frontendStopUri"
>
@ -133,8 +133,8 @@ import frontendPanelProps from "~/components/Stations/Profile/frontendPanelProps
const props = defineProps({
...frontendPanelProps,
np: {
type: Object,
frontendRunning: {
type: Boolean,
required: true
}
});

View File

@ -1,12 +1,12 @@
<template>
<div class="outside-card-header d-flex align-items-center mb-3">
<div
v-if="np.station.listen_url"
v-if="station.listen_url"
class="flex-shrink-0 mr-3"
>
<play-button
icon-class="outlined xl"
:url="np.station.listen_url"
:url="station.listen_url"
is-stream
/>
</div>
@ -44,7 +44,7 @@ import headerPanelProps from "~/components/Stations/Profile/headerPanelProps";
const props = defineProps({
...headerPanelProps,
np: {
station: {
type: Object,
required: true
}

View File

@ -198,38 +198,17 @@
<script setup>
import {BACKEND_LIQUIDSOAP} from '~/components/Entity/RadioAdapters';
import Icon from '~/components/Common/Icon';
import {computed, onMounted, ref} from "vue";
import {useIntervalFn} from "@vueuse/core";
import {computed} from "vue";
import {useTranslate} from "~/vendor/gettext";
import formatTime from "~/functions/formatTime";
import nowPlayingPanelProps from "~/components/Stations/Profile/nowPlayingPanelProps";
import useNowPlaying from "~/functions/useNowPlaying";
const props = defineProps({
...nowPlayingPanelProps,
np: {
type: Object,
required: true
}
});
const npElapsed = ref(0);
onMounted(() => {
useIntervalFn(
() => {
let current_time = Math.floor(Date.now() / 1000);
let np_elapsed = current_time - props.np.now_playing.played_at;
if (np_elapsed < 0) {
np_elapsed = 0;
} else if (np_elapsed >= props.np.now_playing.duration) {
np_elapsed = props.np.now_playing.duration;
}
npElapsed.value = np_elapsed;
},
1000
);
});
const {np, currentTrackDuration, currentTrackElapsed} = useNowPlaying(props);
const {$ngettext} = useTranslate();
@ -237,8 +216,8 @@ const langListeners = computed(() => {
return $ngettext(
'%{listeners} Listener',
'%{listeners} Listeners',
props.np.listeners.total,
{listeners: props.np.listeners.total}
np.value?.listeners?.total ?? 0,
{listeners: np.value?.listeners?.total ?? 0}
);
});
@ -247,17 +226,17 @@ const isLiquidsoap = computed(() => {
});
const timeDisplay = computed(() => {
let time_played = npElapsed.value;
let time_total = props.np.now_playing.duration;
let currentTrackDurationValue = currentTrackDuration.value ?? null;
let currentTrackElapsedValue = currentTrackElapsed.value ?? null;
if (!time_total) {
if (!currentTrackDurationValue) {
return null;
}
if (time_played > time_total) {
time_played = time_total;
if (currentTrackElapsedValue > currentTrackDurationValue) {
currentTrackElapsedValue = currentTrackDurationValue;
}
return formatTime(time_played) + ' / ' + formatTime(time_total);
return formatTime(currentTrackElapsedValue) + ' / ' + formatTime(currentTrackDurationValue);
});
</script>

View File

@ -14,7 +14,7 @@
<col style="width: 78%;">
<col style="width: 20%;">
</colgroup>
<template v-if="np.station.mounts.length > 0">
<template v-if="station.mounts.length > 0">
<thead>
<tr>
<th colspan="2">
@ -27,7 +27,7 @@
</thead>
<tbody>
<tr
v-for="mount in np.station.mounts"
v-for="mount in station.mounts"
:key="mount.id"
class="align-middle"
>
@ -62,7 +62,7 @@
</tbody>
</template>
<template v-if="np.station.remotes.length > 0">
<template v-if="station.remotes.length > 0">
<thead>
<tr>
<th colspan="2">
@ -75,7 +75,7 @@
</thead>
<tbody>
<tr
v-for="remote in np.station.remotes"
v-for="remote in station.remotes"
:key="remote.id"
class="align-middle"
>
@ -110,7 +110,7 @@
</tbody>
</template>
<template v-if="np.station.hls_enabled">
<template v-if="station.hls_enabled">
<thead>
<tr>
<th colspan="2">
@ -126,23 +126,23 @@
<td class="pr-1">
<play-button
icon-class="outlined"
:url="np.station.hls_url"
:url="station.hls_url"
is-stream
is-hls
/>
</td>
<td class="pl-1">
<a
:href="np.station.hls_url"
:href="station.hls_url"
target="_blank"
>{{ np.station.hls_url }}</a>
>{{ station.hls_url }}</a>
</td>
<td class="pl-1 text-right">
<icon
class="sm align-middle"
icon="headset"
/>
<span class="listeners-total pl-1">{{ np.station.hls_listeners }}</span>
<span class="listeners-total pl-1">{{ station.hls_listeners }}</span>
</td>
</tr>
</tbody>
@ -151,14 +151,14 @@
<div class="card-actions">
<a
class="btn btn-outline-primary"
:href="np.station.playlist_pls_url"
:href="station.playlist_pls_url"
>
<icon icon="file_download" />
{{ $gettext('Download PLS') }}
</a>
<a
class="btn btn-outline-primary"
:href="np.station.playlist_m3u_url"
:href="station.playlist_m3u_url"
>
<icon icon="file_download" />
{{ $gettext('Download M3U') }}
@ -172,7 +172,7 @@ import Icon from '~/components/Common/Icon';
import PlayButton from "~/components/Common/PlayButton";
const props = defineProps({
np: {
station: {
type: Object,
required: true
}

View File

@ -1,4 +1,7 @@
import {nowPlayingProps} from "~/functions/useNowPlaying";
export default {
...nowPlayingProps,
backendType: {
type: String,
required: true

View File

@ -15,7 +15,6 @@ final class ProfileAction
{
public function __construct(
private readonly Entity\Repository\StationScheduleRepository $scheduleRepo,
private readonly Entity\ApiGenerator\NowPlayingApiGenerator $nowPlayingApiGenerator,
private readonly Entity\ApiGenerator\StationApiGenerator $stationApiGenerator,
private readonly Adapters $adapters,
) {
@ -31,13 +30,10 @@ final class ProfileAction
$frontend = $this->adapters->getFrontendAdapter($station);
$baseUri = new Uri('');
$nowPlayingApi = $this->nowPlayingApiGenerator->currentOrEmpty($station, $baseUri);
$apiResponse = new Entity\Api\StationProfile();
$apiResponse->fromParentObject($nowPlayingApi);
$apiResponse->station = ($this->stationApiGenerator)($station, $baseUri, true);
$apiResponse->cache = 'database';
$apiResponse->services = new Entity\Api\StationServiceStatus(
null !== $backend && $backend->isRunning($station),
@ -48,7 +44,6 @@ final class ProfileAction
$apiResponse->schedule = $this->scheduleRepo->getUpcomingSchedule($station);
$apiResponse->update();
$apiResponse->resolveUrls($request->getRouter()->getBaseUrl());
return $response->withJson($apiResponse);

View File

@ -8,6 +8,7 @@ use App\Enums\StationPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use App\VueComponent\NowPlayingComponent;
use App\VueComponent\StationFormComponent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface;
@ -19,6 +20,7 @@ final class ProfileController
public function __construct(
private readonly EntityManagerInterface $em,
private readonly StationFormComponent $stationFormComponent,
private readonly NowPlayingComponent $nowPlayingComponent,
private readonly Adapters $adapters,
) {
}
@ -73,6 +75,8 @@ final class ProfileController
id: 'profile',
title: __('Profile'),
props: [
...$this->nowPlayingComponent->getProps($request),
// Common
'backendType' => $station->getBackendType(),
'frontendType' => $station->getFrontendType(),

View File

@ -4,15 +4,23 @@ declare(strict_types=1);
namespace App\Entity\Api;
use App\Entity\Api\NowPlaying\NowPlaying;
use App\Traits\LoadFromParentObject;
use App\Entity\Api\NowPlaying\Station;
use Psr\Http\Message\UriInterface;
final class StationProfile extends NowPlaying
final class StationProfile
{
use LoadFromParentObject;
public Station $station;
public StationServiceStatus $services;
/** @var StationSchedule[] */
public array $schedule = [];
/**
* Iterate through sub-items and re-resolve any Uri instances to reflect base URL changes.
*/
public function resolveUrls(UriInterface $base): void
{
$this->station->resolveUrls($base);
}
}