Rework how storage locations are serialized; live quota on media manager.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-11-17 10:30:21 -06:00
parent 55f04d47e7
commit 9b7d7f7e17
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
11 changed files with 146 additions and 128 deletions

View File

@ -42,7 +42,7 @@ return static function (RouteCollectorProxy $group) {
)
->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
$group->get('/profile', Controller\Api\Stations\ProfileController::class)
$group->get('/profile', Controller\Api\Stations\ProfileAction::class)
->setName('api:stations:profile')
->add(new Middleware\Permissions(Acl::STATION_VIEW, true));
@ -57,6 +57,10 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Stations\ProfileEditController::class . ':putProfileAction'
)->add(new Middleware\Permissions(Acl::STATION_PROFILE, true));
$group->get('/quota[/{type}]', Controller\Api\Stations\GetQuotaAction::class)
->setName('api:stations:quota')
->add(new Middleware\Permissions(Acl::STATION_PROFILE, true));
$group->get('/schedule', Controller\Api\Stations\ScheduleAction::class)
->setName('api:stations:schedule');

View File

@ -28,8 +28,8 @@
<b-form-fieldset>
<b-overlay variant="card" :show="storageLocationsLoading">
<b-form-row>
<b-wrapped-form-group class="col-md-12" id="edit_form_media_storage_location_id"
:field="form.media_storage_location_id">
<b-wrapped-form-group class="col-md-12" id="edit_form_media_storage_location"
:field="form.media_storage_location">
<template #label="{lang}">
<translate :key="lang">Media Storage Location</translate>
</template>
@ -39,8 +39,8 @@
</template>
</b-wrapped-form-group>
<b-wrapped-form-group class="col-md-12" id="edit_form_recordings_storage_location_id"
:field="form.recordings_storage_location_id">
<b-wrapped-form-group class="col-md-12" id="edit_form_recordings_storage_location"
:field="form.recordings_storage_location">
<template #label="{lang}">
<translate :key="lang">Live Recordings Storage Location</translate>
</template>
@ -50,8 +50,8 @@
</template>
</b-wrapped-form-group>
<b-wrapped-form-group class="col-md-12" id="edit_form_podcasts_storage_location_id"
:field="form.podcasts_storage_location_id">
<b-wrapped-form-group class="col-md-12" id="edit_form_podcasts_storage_location"
:field="form.podcasts_storage_location">
<template #label="{lang}">
<translate :key="lang">Podcasts Storage Location</translate>
</template>

View File

@ -168,14 +168,14 @@ export default {
if (this.showAdminTab) {
const adminValidations = {
form: {
media_storage_location_id: {},
recordings_storage_location_id: {},
podcasts_storage_location_id: {},
media_storage_location: {},
recordings_storage_location: {},
podcasts_storage_location: {},
is_enabled: {},
},
adminTab: [
'form.media_storage_location_id', 'form.recordings_storage_location_id',
'form.podcasts_storage_location_id', 'form.is_enabled'
'form.media_storage_location', 'form.recordings_storage_location',
'form.podcasts_storage_location', 'form.is_enabled'
]
};
@ -251,7 +251,6 @@ export default {
sc_user_id: '',
source_pw: '',
admin_pw: '',
},
backend_type: BACKEND_LIQUIDSOAP,
backend_config: {
@ -298,9 +297,9 @@ export default {
if (this.showAdminTab) {
const adminForm = {
media_storage_location_id: '',
recordings_storage_location_id: '',
podcasts_storage_location_id: '',
media_storage_location: '',
recordings_storage_location: '',
podcasts_storage_location: '',
is_enabled: true,
};
_.merge(form, adminForm);

View File

@ -9,9 +9,10 @@
<translate key="lang_title">Music Files</translate>
</h2>
</div>
<div class="col-md-5 text-right text-white-50">
<template v-if="spaceTotal">
<b-progress class="mb-1" :value="spacePercent" show-progress height="20px"></b-progress>
<div v-if="!quotaLoading" class="col-md-5 text-right text-white-50">
<template v-if="quota.available">
<b-progress class="mb-1" :value="quota.used_percent" show-progress
height="20px"></b-progress>
{{ langSpaceUsed }}
</template>
@ -159,6 +160,7 @@ import Icon from '~/components/Common/Icon';
import AlbumArt from '~/components/Common/AlbumArt';
import PlayButton from "~/components/Common/PlayButton";
import {DateTime} from 'luxon';
import mergeExisting from "~/functions/mergeExisting";
export default {
components: {
@ -199,6 +201,10 @@ export default {
type: String,
required: true
},
quotaUrl: {
type: String,
required: true
},
initialPlaylists: {
type: Array,
required: false,
@ -215,12 +221,7 @@ export default {
default: () => []
},
stationTimeZone: String,
spacePercent: Number,
spaceUsed: String,
spaceTotal: String,
filesCount: Number,
showSftp: Boolean,
sftpUrl: String
},
@ -283,6 +284,12 @@ export default {
files: [],
directories: []
},
quotaLoading: true,
quota: {
used: null,
used_percent: null,
available: null
},
currentDirectory: '',
searchPhrase: null
};
@ -294,6 +301,7 @@ export default {
window.removeEventListener('hashchange', this.onHashChange);
},
mounted () {
this.loadQuotas();
this.onHashChange();
},
computed: {
@ -310,22 +318,29 @@ export default {
return this.$gettext('View tracks in playlist');
},
langSpaceUsed() {
let lang = (this.spaceTotal)
let lang = (this.quota.available)
? this.$gettext('%{spaceUsed} of %{spaceTotal} Used (%{filesCount} Files)')
: this.$gettext('%{spaceUsed} Used (%{filesCount} Files)');
return this.$gettextInterpolate(lang, {
spaceUsed: this.spaceUsed,
spaceTotal: this.spaceTotal,
spaceUsed: this.quota.used,
spaceTotal: this.quota.available,
filesCount: this.filesCount
});
},
},
methods: {
formatFileSize (size) {
formatFileSize(size) {
return formatFileSize(size);
},
onRowSelected (items) {
loadQuotas() {
this.axios.get(this.quotaUrl)
.then((resp) => {
this.quota = mergeExisting(this.quota, resp.data);
this.quotaLoading = false;
});
},
onRowSelected(items) {
let splitItems = _.partition(items, 'is_dir');
this.selectedItems = {
@ -334,13 +349,14 @@ export default {
directories: _.map(splitItems[0], 'path')
};
},
onRefreshed () {
onRefreshed() {
this.$eventHub.$emit('refreshed');
},
onTriggerNavigate () {
this.$refs.datatable.navigate();
},
onTriggerRelist () {
this.loadQuotas();
this.$refs.datatable.relist();
},
onAddPlaylist (row) {

View File

@ -21,7 +21,7 @@ class StorageLocationsAction extends StationsController
$newStorageLocationMessage = __('Create a new storage location based on the base directory.');
$storageLocations = [];
foreach (Station::getStorageLocationTypes() as $locationKey => $locationType) {
foreach (Station::getStorageLocationTypes() as $locationType => $locationKey) {
$storageLocations[$locationKey] = $storageLocationRepo->fetchSelectByType(
$locationType,
true,

View File

@ -165,12 +165,18 @@ class StationsController extends AbstractAdminApiCrudController
'has_started',
];
foreach (Entity\Station::getStorageLocationTypes() as $storageLocationType => $locationKey) {
$context[AbstractNormalizer::CALLBACKS][$locationKey] = fn(
array $value
) => $value['id'];
}
return parent::toArray($record, $context);
}
protected function fromArray(array $data, ?object $record = null, array $context = []): object
{
foreach (Entity\Station::getStorageLocationTypes() as $locationKey => $locationType) {
foreach (Entity\Station::getStorageLocationTypes() as $locationKey) {
$idKey = $locationKey . '_id';
if (!empty($data[$idKey])) {
$data[$locationKey] = $data[$idKey];

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Entity\StorageLocation;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
class GetQuotaAction
{
public function __invoke(
ServerRequest $request,
Response $response,
string $type = StorageLocation::TYPE_STATION_MEDIA
): ResponseInterface {
$storageLocation = $request->getStation()->getStorageLocation($type);
return $response->withJson([
'used' => $storageLocation->getStorageUsed(),
'used_bytes' => (string)$storageLocation->getStorageUsedBytes(),
'used_percent' => $storageLocation->getStorageUsePercentage(),
'available' => $storageLocation->getStorageAvailable(),
'available_bytes' => (string)$storageLocation->getStorageAvailableBytes(),
'quota' => $storageLocation->getStorageQuota(),
'quota_bytes' => (string)$storageLocation->getStorageQuotaBytes(),
]);
}
}

View File

@ -10,7 +10,7 @@ use App\Http\ServerRequest;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\ResponseInterface;
class ProfileController
class ProfileAction
{
public function __invoke(
ServerRequest $request,

View File

@ -41,8 +41,6 @@ class FilesAction
)->setParameter('storageLocation', $station->getMediaStorageLocation())
->getSingleScalarResult();
$mediaStorage = $station->getMediaStorageLocation();
$router = $request->getRouter();
return $request->getView()->renderVuePage(
@ -51,22 +49,22 @@ class FilesAction
id: 'media-manager',
title: __('Music Files'),
props: [
'listUrl' => (string)$router->fromHere('api:stations:files:list'),
'batchUrl' => (string)$router->fromHere('api:stations:files:batch'),
'uploadUrl' => (string)$router->fromHere('api:stations:files:upload'),
'listUrl' => (string)$router->fromHere('api:stations:files:list'),
'batchUrl' => (string)$router->fromHere('api:stations:files:batch'),
'uploadUrl' => (string)$router->fromHere('api:stations:files:upload'),
'listDirectoriesUrl' => (string)$router->fromHere('api:stations:files:directories'),
'mkdirUrl' => (string)$router->fromHere('api:stations:files:mkdir'),
'renameUrl' => (string)$router->fromHere('api:stations:files:rename'),
'initialPlaylists' => $playlists,
'customFields' => $customFieldRepo->fetchArray(),
'validMimeTypes' => MimeType::getProcessableTypes(),
'stationTimeZone' => $station->getTimezone(),
'spacePercent' => $mediaStorage->getStorageUsePercentage(),
'spaceUsed' => $mediaStorage->getStorageUsed(),
'spaceTotal' => $mediaStorage->getStorageAvailable(),
'filesCount' => (int)$files_count,
'showSftp' => SftpGo::isSupportedForStation($station),
'sftpUrl' => (string)$router->fromHere('stations:sftp_users:index'),
'mkdirUrl' => (string)$router->fromHere('api:stations:files:mkdir'),
'renameUrl' => (string)$router->fromHere('api:stations:files:rename'),
'quotaUrl' => (string)$router->fromHere('api:stations:quota', [
'type' => Entity\StorageLocation::TYPE_STATION_MEDIA,
]),
'initialPlaylists' => $playlists,
'customFields' => $customFieldRepo->fetchArray(),
'validMimeTypes' => MimeType::getProcessableTypes(),
'stationTimeZone' => $station->getTimezone(),
'filesCount' => (int)$files_count,
'showSftp' => SftpGo::isSupportedForStation($station),
'sftpUrl' => (string)$router->fromHere('stations:sftp_users:index'),
],
);
}

View File

@ -15,7 +15,6 @@ use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use OpenApi\Annotations as OA;
use RuntimeException;
@ -276,10 +275,6 @@ class Station implements Stringable, IdentifiableEntityInterface
#[ORM\OrderBy(['timestamp_start' => 'desc'])]
protected Collection $history;
#[ORM\Column(nullable: true)]
#[Serializer\Groups([EntityGroupsInterface::GROUP_ADMIN, EntityGroupsInterface::GROUP_ALL])]
protected ?int $media_storage_location_id = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(
name: 'media_storage_location_id',
@ -289,11 +284,8 @@ class Station implements Stringable, IdentifiableEntityInterface
)]
#[DeepNormalize(true)]
#[Serializer\MaxDepth(1)]
protected ?StorageLocation $media_storage_location = null;
#[ORM\Column(nullable: true)]
#[Serializer\Groups([EntityGroupsInterface::GROUP_ADMIN, EntityGroupsInterface::GROUP_ALL])]
protected ?int $recordings_storage_location_id = null;
protected ?StorageLocation $media_storage_location = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(
@ -304,11 +296,8 @@ class Station implements Stringable, IdentifiableEntityInterface
)]
#[DeepNormalize(true)]
#[Serializer\MaxDepth(1)]
protected ?StorageLocation $recordings_storage_location = null;
#[ORM\Column(nullable: true)]
#[Serializer\Groups([EntityGroupsInterface::GROUP_ADMIN, EntityGroupsInterface::GROUP_ALL])]
protected ?int $podcasts_storage_location_id = null;
protected ?StorageLocation $recordings_storage_location = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(
@ -319,6 +308,7 @@ class Station implements Stringable, IdentifiableEntityInterface
)]
#[DeepNormalize(true)]
#[Serializer\MaxDepth(1)]
#[Serializer\Groups([EntityGroupsInterface::GROUP_ADMIN, EntityGroupsInterface::GROUP_ALL])]
protected ?StorageLocation $podcasts_storage_location = null;
#[ORM\OneToMany(mappedBy: 'station', targetEntity: StationStreamer::class)]
@ -894,88 +884,62 @@ class Station implements Stringable, IdentifiableEntityInterface
}
}
public function getMediaStorageLocationId(): ?int
{
return $this->media_storage_location_id;
}
public function setMediaStorageLocationId(?int $media_storage_location_id): void
{
$this->media_storage_location_id = $media_storage_location_id;
}
public function getMediaStorageLocation(): StorageLocation
{
if (null === $this->media_storage_location) {
throw new \RuntimeException('Media storage location has not been configured yet.');
}
return $this->media_storage_location;
return $this->getStorageLocation(StorageLocation::TYPE_STATION_MEDIA);
}
public function setMediaStorageLocation(StorageLocation $storageLocation): void
{
if (StorageLocation::TYPE_STATION_MEDIA !== $storageLocation->getType()) {
throw new InvalidArgumentException('Storage location must be for station media.');
}
$this->media_storage_location = $storageLocation;
}
public function getRecordingsStorageLocationId(): ?int
{
return $this->recordings_storage_location_id;
}
public function setRecordingsStorageLocationId(?int $recordings_storage_location_id): void
{
$this->recordings_storage_location_id = $recordings_storage_location_id;
$this->setStorageLocation(StorageLocation::TYPE_STATION_MEDIA, $storageLocation);
}
public function getRecordingsStorageLocation(): StorageLocation
{
if (null === $this->recordings_storage_location) {
throw new \RuntimeException('Recordings storage location has not been configured yet.');
}
return $this->recordings_storage_location;
return $this->getStorageLocation(StorageLocation::TYPE_STATION_RECORDINGS);
}
public function setRecordingsStorageLocation(StorageLocation $storageLocation): void
{
if (StorageLocation::TYPE_STATION_RECORDINGS !== $storageLocation->getType()) {
throw new InvalidArgumentException('Storage location must be for station live recordings.');
}
$this->recordings_storage_location = $storageLocation;
}
public function getPodcastsStorageLocationId(): ?int
{
return $this->podcasts_storage_location_id;
}
public function setPodcastsStorageLocationId(?int $podcasts_storage_location_id): void
{
$this->podcasts_storage_location_id = $podcasts_storage_location_id;
$this->setStorageLocation(StorageLocation::TYPE_STATION_RECORDINGS, $storageLocation);
}
public function getPodcastsStorageLocation(): StorageLocation
{
if (null === $this->podcasts_storage_location) {
throw new \RuntimeException('Podcasts storage location has not been configured yet.');
}
return $this->podcasts_storage_location;
return $this->getStorageLocation(StorageLocation::TYPE_STATION_PODCASTS);
}
public function setPodcastsStorageLocation(StorageLocation $storageLocation): void
{
if (StorageLocation::TYPE_STATION_PODCASTS !== $storageLocation->getType()) {
throw new InvalidArgumentException('Storage location must be for station podcasts.');
$this->setStorageLocation(StorageLocation::TYPE_STATION_PODCASTS, $storageLocation);
}
public function getStorageLocation(string $type): StorageLocation
{
$supportedTypes = self::getStorageLocationTypes();
if (!isset($supportedTypes[$type])) {
throw new \InvalidArgumentException(sprintf('Invalid type: %s', $type));
}
$this->podcasts_storage_location = $storageLocation;
$record = $this->{$supportedTypes[$type]};
if (null === $record) {
throw new \RuntimeException(sprintf('Storage location for type %s has not been configured yet.', $type));
}
return $record;
}
public function setStorageLocation(string $type, StorageLocation $newStorageLocation): void
{
if ($type !== $newStorageLocation->getType()) {
throw new \InvalidArgumentException(sprintf('Specified location is not of type %s.', $type));
}
$supportedTypes = self::getStorageLocationTypes();
if (!isset($supportedTypes[$type])) {
throw new \InvalidArgumentException(sprintf('Invalid type: %s', $type));
}
$this->{$supportedTypes[$type]} = $newStorageLocation;
}
/** @return StorageLocation[] */
@ -991,9 +955,9 @@ class Station implements Stringable, IdentifiableEntityInterface
public static function getStorageLocationTypes(): array
{
return [
'media_storage_location' => StorageLocation::TYPE_STATION_MEDIA,
'recordings_storage_location' => StorageLocation::TYPE_STATION_RECORDINGS,
'podcasts_storage_location' => StorageLocation::TYPE_STATION_PODCASTS,
StorageLocation::TYPE_STATION_MEDIA => 'media_storage_location',
StorageLocation::TYPE_STATION_RECORDINGS => 'recordings_storage_location',
StorageLocation::TYPE_STATION_PODCASTS => 'podcasts_storage_location',
];
}

View File

@ -311,14 +311,14 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
public function getStorageAvailable(): string
{
$raw_size = $this->getRawStorageAvailable();
$raw_size = $this->getStorageAvailableBytes();
return ($raw_size instanceof BigInteger)
? Quota::getReadableSize($raw_size)
: '';
}
public function getRawStorageAvailable(): ?BigInteger
public function getStorageAvailableBytes(): ?BigInteger
{
$quota = $this->getStorageQuotaBytes();
@ -339,7 +339,7 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
public function isStorageFull(): bool
{
$available = $this->getRawStorageAvailable();
$available = $this->getStorageAvailableBytes();
if ($available === null) {
return false;
}
@ -352,7 +352,7 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
public function getStorageUsePercentage(): int
{
$storageUsed = $this->getStorageUsedBytes();
$storageAvailable = $this->getRawStorageAvailable();
$storageAvailable = $this->getStorageAvailableBytes();
if (null === $storageAvailable) {
return 0;