Move requests report into Vue component.
This commit is contained in:
parent
8d3cab6e76
commit
c94e2edf19
|
@ -597,6 +597,12 @@ return [
|
|||
// Auto-managed by Assets
|
||||
],
|
||||
|
||||
'Vue_StationsReportsRequests' => [
|
||||
'order' => 10,
|
||||
'require' => ['vue-component-common', 'uses-api', 'bootstrap-vue', 'moment'],
|
||||
// Auto-managed by Assets
|
||||
],
|
||||
|
||||
'Vue_StationsReportsOverview' => [
|
||||
'order' => 10,
|
||||
'require' => ['vue-component-common', 'uses-api', 'bootstrap-vue', 'chartjs'],
|
||||
|
|
|
@ -243,10 +243,8 @@ return static function (RouteCollectorProxy $app) {
|
|||
$group->post('/clear', Controller\Api\Stations\QueueController::class . ':clearAction')
|
||||
->setName('api:stations:queue:clear');
|
||||
|
||||
$group->get('/{id}', Controller\Api\Stations\QueueController::class . ':getAction')
|
||||
$group->delete('/{id}', Controller\Api\Stations\QueueController::class . ':deleteAction')
|
||||
->setName('api:stations:queue:record');
|
||||
|
||||
$group->delete('/{id}', Controller\Api\Stations\QueueController::class . ':deleteAction');
|
||||
}
|
||||
)->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
|
||||
|
||||
|
@ -572,6 +570,26 @@ return static function (RouteCollectorProxy $app) {
|
|||
$group->group(
|
||||
'/reports',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->group(
|
||||
'/requests',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get(
|
||||
'',
|
||||
Controller\Api\Stations\Reports\RequestsController::class . ':listAction'
|
||||
)->setName('api:stations:reports:requests');
|
||||
|
||||
$group->post(
|
||||
'/clear',
|
||||
Controller\Api\Stations\Reports\RequestsController::class . ':clearAction'
|
||||
)->setName('api:stations:reports:requests:clear');
|
||||
|
||||
$group->delete(
|
||||
'/{request_id}',
|
||||
Controller\Api\Stations\Reports\RequestsController::class . ':deleteAction'
|
||||
)->setName('api:stations:reports:requests:delete');
|
||||
}
|
||||
)->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
|
||||
|
||||
$group->get(
|
||||
'/overview/charts',
|
||||
Controller\Api\Stations\Reports\Overview\ChartsAction::class
|
||||
|
|
|
@ -109,20 +109,8 @@ return static function (RouteCollectorProxy $app) {
|
|||
)
|
||||
->setName('stations:reports:soundexchange');
|
||||
|
||||
$group->get('/requests', Controller\Stations\Reports\RequestsController::class)
|
||||
$group->get('/requests', Controller\Stations\Reports\RequestsAction::class)
|
||||
->setName('stations:reports:requests');
|
||||
|
||||
$group->get(
|
||||
'/requests/delete/{request_id}/{csrf}',
|
||||
Controller\Stations\Reports\RequestsController::class . ':deleteAction'
|
||||
)
|
||||
->setName('stations:reports:requests:delete');
|
||||
|
||||
$group->get(
|
||||
'/requests/clear/{csrf}',
|
||||
Controller\Stations\Reports\RequestsController::class . ':clearAction'
|
||||
)
|
||||
->setName('stations:reports:requests:clear');
|
||||
}
|
||||
)->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<translate key="lang_btn_clear_requests">Clear Upcoming Song Queue</translate>
|
||||
</b-button>
|
||||
</div>
|
||||
<data-table ref="datatable" id="station_queue" :fields="fields" :api-url="listUrl" handle-client-side>
|
||||
<data-table ref="datatable" id="station_queue" :fields="fields" :api-url="listUrl">
|
||||
<template #cell(actions)="row">
|
||||
<b-button-group>
|
||||
<b-button v-if="row.item.log" size="sm" variant="primary"
|
||||
|
@ -60,12 +60,11 @@ import handleAxiosError from '../Function/handleAxiosError';
|
|||
import Icon from "../Common/Icon";
|
||||
|
||||
export default {
|
||||
name: 'StationPlaylists',
|
||||
name: 'StationQueue',
|
||||
components: {QueueLogsModal, DataTable, Icon},
|
||||
props: {
|
||||
listUrl: String,
|
||||
clearUrl: String,
|
||||
locale: String,
|
||||
stationTimeZone: String
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-card no-body>
|
||||
<b-card-header header-bg-variant="primary-dark">
|
||||
<h2 class="card-title" key="lang_queue" v-translate>Song Requests</h2>
|
||||
</b-card-header>
|
||||
<div class="card-actions">
|
||||
<b-button variant="outline-danger" @click="doClear()">
|
||||
<icon icon="remove"></icon>
|
||||
<translate key="lang_btn_clear_requests">Clear Pending Requests</translate>
|
||||
</b-button>
|
||||
</div>
|
||||
<data-table ref="datatable" id="station_queue" :fields="fields" :api-url="listUrl">
|
||||
<template #cell(timestamp)="row">
|
||||
{{ formatTime(row.item.timestamp) }}
|
||||
</template>
|
||||
<template #cell(played_at)="row">
|
||||
<span v-if="row.item.played_at === 0">
|
||||
<translate key="lang_item_not_played">Not Played</translate>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ formatTime(row.item.played_at) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #cell(song_title)="row">
|
||||
<div v-if="row.item.track.title">
|
||||
<b>{{ row.item.track.title }}</b><br>
|
||||
{{ row.item.track.artist }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ row.item.track.text }}
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(ip)="row">
|
||||
{{ row.item.ip }}
|
||||
</template>
|
||||
<template #cell(actions)="row">
|
||||
<b-button-group>
|
||||
<b-button v-if="row.item.played_at === 0" size="sm" variant="danger"
|
||||
@click.prevent="doDelete(row.item.links.delete)">
|
||||
<translate key="lang_btn_delete">Delete</translate>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</template>
|
||||
</data-table>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataTable from '../../Common/DataTable';
|
||||
import handleAxiosError from '../../Function/handleAxiosError';
|
||||
import Icon from "../../Common/Icon";
|
||||
|
||||
export default {
|
||||
name: 'StationRequests',
|
||||
components: {DataTable, Icon},
|
||||
props: {
|
||||
listUrl: String,
|
||||
clearUrl: String,
|
||||
stationTimeZone: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fields: [
|
||||
{key: 'timestamp', label: this.$gettext('Date Requested'), sortable: false},
|
||||
{key: 'played_at', label: this.$gettext('Date Played'), sortable: false},
|
||||
{key: 'song_title', isRowHeader: true, label: this.$gettext('Song Title'), sortable: false},
|
||||
{key: 'ip', label: this.$gettext('Requester IP'), sortable: false},
|
||||
{key: 'actions', label: this.$gettext('Actions'), sortable: false}
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
moment.relativeTimeThreshold('ss', 1);
|
||||
moment.relativeTimeRounding(function (value) {
|
||||
return Math.round(value * 10) / 10;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
formatTime(time) {
|
||||
return moment.unix(time).tz(this.stationTimeZone).format('lll');
|
||||
},
|
||||
doDelete(url) {
|
||||
let buttonText = this.$gettext('Delete');
|
||||
let buttonConfirmText = this.$gettext('Delete request?');
|
||||
|
||||
Swal.fire({
|
||||
title: buttonConfirmText,
|
||||
confirmButtonText: buttonText,
|
||||
confirmButtonColor: '#e64942',
|
||||
showCancelButton: true,
|
||||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.$refs.datatable.refresh();
|
||||
}).catch((err) => {
|
||||
handleAxiosError(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
doClear() {
|
||||
let buttonText = this.$gettext('Clear');
|
||||
let buttonConfirmText = this.$gettext('Clear all pending requests?');
|
||||
|
||||
Swal.fire({
|
||||
title: buttonConfirmText,
|
||||
confirmButtonText: buttonText,
|
||||
confirmButtonColor: '#e64942',
|
||||
showCancelButton: true,
|
||||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
this.axios.post(this.clearUrl).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.$refs.datatable.refresh();
|
||||
}).catch((err) => {
|
||||
handleAxiosError(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -24,6 +24,7 @@ module.exports = {
|
|||
StationsQueue: './vue/Stations/Queue.vue',
|
||||
StationsRemotes: './vue/Stations/Remotes.vue',
|
||||
StationsStreamers: './vue/Stations/Streamers.vue',
|
||||
StationsReportsRequests: './vue/Stations/Reports/Requests.vue',
|
||||
StationsReportsOverview: './vue/Stations/Reports/Overview.vue'
|
||||
},
|
||||
resolve: {
|
||||
|
|
|
@ -44,14 +44,14 @@ abstract class AbstractApiCrudController
|
|||
): ResponseInterface {
|
||||
$paginator = Paginator::fromQuery($query, $request);
|
||||
|
||||
$is_bootgrid = $paginator->isFromBootgrid();
|
||||
$is_internal = ('true' === $request->getParam('internal', 'false'));
|
||||
$isBootgrid = $paginator->isFromBootgrid();
|
||||
$isInternal = ('true' === $request->getParam('internal', 'false'));
|
||||
|
||||
$postProcessor ??= function ($row) use ($is_bootgrid, $is_internal, $request) {
|
||||
$postProcessor ??= function ($row) use ($isBootgrid, $isInternal, $request) {
|
||||
$return = $this->viewRecord($row, $request);
|
||||
|
||||
// Older jQuery Bootgrid requests should be "flattened".
|
||||
if ($is_bootgrid && !$is_internal) {
|
||||
if ($isBootgrid && !$isInternal) {
|
||||
return Utilities\Arrays::flattenArray($return, '_');
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\Reports;
|
||||
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Paginator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class RequestsController
|
||||
{
|
||||
public function __construct(
|
||||
protected EntityManagerInterface $em,
|
||||
protected Entity\Repository\StationRequestRepository $requestRepo
|
||||
) {
|
||||
}
|
||||
|
||||
public function listAction(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
$requests = $this->em->createQuery(
|
||||
<<<'DQL'
|
||||
SELECT sr, sm
|
||||
FROM App\Entity\StationRequest sr
|
||||
JOIN sr.track sm
|
||||
WHERE sr.station = :station
|
||||
ORDER BY sr.timestamp DESC
|
||||
DQL
|
||||
)->setParameter('station', $station)
|
||||
->getArrayResult();
|
||||
|
||||
$paginator = Paginator::fromArray($requests, $request);
|
||||
|
||||
$router = $request->getRouter();
|
||||
$postProcessor = function ($row) use ($router) {
|
||||
$row['links'] = [
|
||||
'delete' => (string)$router->fromHere(
|
||||
'api:stations:reports:requests:delete',
|
||||
['request_id' => $row['id']]
|
||||
),
|
||||
];
|
||||
|
||||
return $row;
|
||||
};
|
||||
$paginator->setPostprocessor($postProcessor);
|
||||
|
||||
return $paginator->write($response);
|
||||
}
|
||||
|
||||
public function deleteAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int $request_id
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
$media = $this->requestRepo->getPendingRequest($request_id, $station);
|
||||
|
||||
if ($media instanceof Entity\StationRequest) {
|
||||
$this->em->remove($media);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
return $response->withJson(Entity\Api\Status::deleted());
|
||||
}
|
||||
|
||||
public function clearAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
$this->requestRepo->clearPendingRequests($station);
|
||||
|
||||
return $response->withJson(Entity\Api\Status::deleted());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Stations\Reports;
|
||||
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class RequestsAction
|
||||
{
|
||||
public function __invoke(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
$station = $request->getStation();
|
||||
return $request->getView()->renderToResponse(
|
||||
$response,
|
||||
'stations/reports/requests',
|
||||
[
|
||||
'stationTz' => $station->getTimezone(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Stations\Reports;
|
||||
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Session\Flash;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class RequestsController
|
||||
{
|
||||
protected string $csrf_namespace = 'stations_requests';
|
||||
|
||||
public function __construct(
|
||||
protected EntityManagerInterface $em
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
$requests = $this->em->createQuery(
|
||||
<<<'DQL'
|
||||
SELECT sr, sm
|
||||
FROM App\Entity\StationRequest sr
|
||||
JOIN sr.track sm
|
||||
WHERE sr.station_id = :station_id
|
||||
ORDER BY sr.timestamp DESC
|
||||
DQL
|
||||
)->setParameter('station_id', $station->getId())
|
||||
->getArrayResult();
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/reports/requests', [
|
||||
'requests' => $requests,
|
||||
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int $request_id,
|
||||
string $csrf
|
||||
): ResponseInterface {
|
||||
$request->getCsrf()->verify($csrf, $this->csrf_namespace);
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
$media = $this->em->getRepository(Entity\StationRequest::class)->findOneBy([
|
||||
'id' => $request_id,
|
||||
'station_id' => $station->getId(),
|
||||
'played_at' => 0,
|
||||
]);
|
||||
|
||||
if ($media instanceof Entity\StationRequest) {
|
||||
$this->em->remove($media);
|
||||
$this->em->flush();
|
||||
|
||||
$request->getFlash()->addMessage('<b>Request deleted!</b>', Flash::SUCCESS);
|
||||
}
|
||||
|
||||
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:reports:requests'));
|
||||
}
|
||||
|
||||
public function clearAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $csrf
|
||||
): ResponseInterface {
|
||||
$request->getCsrf()->verify($csrf, $this->csrf_namespace);
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
$this->em->createQuery(
|
||||
<<<'DQL'
|
||||
DELETE FROM App\Entity\StationRequest sr
|
||||
WHERE sr.station = :station
|
||||
AND sr.played_at = 0
|
||||
DQL
|
||||
)->setParameter('station', $station)
|
||||
->execute();
|
||||
|
||||
$request->getFlash()->addMessage('<b>All pending requests cleared.</b>', Flash::SUCCESS);
|
||||
|
||||
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:reports:requests'));
|
||||
}
|
||||
}
|
|
@ -10,6 +10,9 @@ use Carbon\CarbonImmutable;
|
|||
use Carbon\CarbonInterface;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\Analytics>
|
||||
*/
|
||||
class AnalyticsRepository extends Repository
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -4,6 +4,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\Repository;
|
||||
|
||||
use App\Entity;
|
||||
|
||||
/**
|
||||
* @extends AbstractSplitTokenRepository<Entity\ApiKey>
|
||||
*/
|
||||
class ApiKeyRepository extends AbstractSplitTokenRepository
|
||||
{
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ namespace App\Entity\Repository;
|
|||
use App\Doctrine\Repository;
|
||||
use App\Entity;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\CustomField>
|
||||
*/
|
||||
class CustomFieldRepository extends Repository
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,9 @@ use Carbon\CarbonImmutable;
|
|||
use DateTimeInterface;
|
||||
use NowPlaying\Result\Client;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\Listener>
|
||||
*/
|
||||
class ListenerRepository extends Repository
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -6,10 +6,7 @@ namespace App\Entity\Repository;
|
|||
|
||||
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||
use App\Doctrine\Repository;
|
||||
use App\Entity\Podcast;
|
||||
use App\Entity\PodcastEpisode;
|
||||
use App\Entity\Station;
|
||||
use App\Entity\StorageLocation;
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use Azura\Files\ExtendedFilesystemInterface;
|
||||
use Intervention\Image\Constraint;
|
||||
|
@ -18,6 +15,9 @@ use League\Flysystem\UnableToDeleteFile;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\PodcastEpisode>
|
||||
*/
|
||||
class PodcastEpisodeRepository extends Repository
|
||||
{
|
||||
public function __construct(
|
||||
|
@ -30,7 +30,7 @@ class PodcastEpisodeRepository extends Repository
|
|||
parent::__construct($entityManager, $serializer, $environment, $logger);
|
||||
}
|
||||
|
||||
public function fetchEpisodeForStation(Station $station, string $episodeId): ?PodcastEpisode
|
||||
public function fetchEpisodeForStation(Entity\Station $station, string $episodeId): ?Entity\PodcastEpisode
|
||||
{
|
||||
return $this->fetchEpisodeForStorageLocation(
|
||||
$station->getPodcastsStorageLocation(),
|
||||
|
@ -39,9 +39,9 @@ class PodcastEpisodeRepository extends Repository
|
|||
}
|
||||
|
||||
public function fetchEpisodeForStorageLocation(
|
||||
StorageLocation $storageLocation,
|
||||
Entity\StorageLocation $storageLocation,
|
||||
string $episodeId
|
||||
): ?PodcastEpisode {
|
||||
): ?Entity\PodcastEpisode {
|
||||
return $this->em->createQuery(
|
||||
<<<'DQL'
|
||||
SELECT pe
|
||||
|
@ -56,13 +56,13 @@ class PodcastEpisodeRepository extends Repository
|
|||
}
|
||||
|
||||
/**
|
||||
* @return PodcastEpisode[]
|
||||
* @return Entity\PodcastEpisode[]
|
||||
*/
|
||||
public function fetchPublishedEpisodesForPodcast(Podcast $podcast): array
|
||||
public function fetchPublishedEpisodesForPodcast(Entity\Podcast $podcast): array
|
||||
{
|
||||
$episodes = $this->em->createQueryBuilder()
|
||||
->select('pe')
|
||||
->from(PodcastEpisode::class, 'pe')
|
||||
->from(Entity\PodcastEpisode::class, 'pe')
|
||||
->where('pe.podcast = :podcast')
|
||||
->setParameter('podcast', $podcast)
|
||||
->getQuery()
|
||||
|
@ -70,14 +70,14 @@ class PodcastEpisodeRepository extends Repository
|
|||
|
||||
return array_filter(
|
||||
$episodes,
|
||||
static function (PodcastEpisode $episode) {
|
||||
static function (Entity\PodcastEpisode $episode) {
|
||||
return $episode->isPublished();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function writeEpisodeArt(
|
||||
PodcastEpisode $episode,
|
||||
Entity\PodcastEpisode $episode,
|
||||
string $rawArtworkString
|
||||
): void {
|
||||
$episodeArtwork = $this->imageManager->make($rawArtworkString);
|
||||
|
@ -89,7 +89,7 @@ class PodcastEpisodeRepository extends Repository
|
|||
}
|
||||
);
|
||||
|
||||
$episodeArtworkPath = PodcastEpisode::getArtPath($episode->getIdRequired());
|
||||
$episodeArtworkPath = Entity\PodcastEpisode::getArtPath($episode->getIdRequired());
|
||||
$episodeArtworkStream = $episodeArtwork->stream('jpg');
|
||||
|
||||
$fsPodcasts = $episode->getPodcast()->getStorageLocation()->getFilesystem();
|
||||
|
@ -99,10 +99,10 @@ class PodcastEpisodeRepository extends Repository
|
|||
}
|
||||
|
||||
public function removeEpisodeArt(
|
||||
PodcastEpisode $episode,
|
||||
Entity\PodcastEpisode $episode,
|
||||
?ExtendedFilesystemInterface $fs = null
|
||||
): void {
|
||||
$artworkPath = PodcastEpisode::getArtPath($episode->getIdRequired());
|
||||
$artworkPath = Entity\PodcastEpisode::getArtPath($episode->getIdRequired());
|
||||
|
||||
$fs ??= $episode->getPodcast()->getStorageLocation()->getFilesystem();
|
||||
|
||||
|
@ -115,7 +115,7 @@ class PodcastEpisodeRepository extends Repository
|
|||
}
|
||||
|
||||
public function delete(
|
||||
PodcastEpisode $episode,
|
||||
Entity\PodcastEpisode $episode,
|
||||
?ExtendedFilesystemInterface $fs = null
|
||||
): void {
|
||||
$fs ??= $episode->getPodcast()->getStorageLocation()->getFilesystem();
|
||||
|
|
|
@ -6,8 +6,7 @@ namespace App\Entity\Repository;
|
|||
|
||||
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||
use App\Doctrine\Repository;
|
||||
use App\Entity\PodcastEpisode;
|
||||
use App\Entity\PodcastMedia;
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Exception\InvalidPodcastMediaFileException;
|
||||
use App\Media\MetadataManager;
|
||||
|
@ -17,6 +16,9 @@ use League\Flysystem\UnableToDeleteFile;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\PodcastMedia>
|
||||
*/
|
||||
class PodcastMediaRepository extends Repository
|
||||
{
|
||||
public function __construct(
|
||||
|
@ -32,7 +34,7 @@ class PodcastMediaRepository extends Repository
|
|||
}
|
||||
|
||||
public function upload(
|
||||
PodcastEpisode $episode,
|
||||
Entity\PodcastEpisode $episode,
|
||||
string $originalPath,
|
||||
string $uploadPath,
|
||||
?ExtendedFilesystemInterface $fs = null
|
||||
|
@ -52,7 +54,7 @@ class PodcastMediaRepository extends Repository
|
|||
}
|
||||
|
||||
$existingMedia = $episode->getMedia();
|
||||
if ($existingMedia instanceof PodcastMedia) {
|
||||
if ($existingMedia instanceof Entity\PodcastMedia) {
|
||||
$this->delete($existingMedia, $fs);
|
||||
$episode->setMedia(null);
|
||||
}
|
||||
|
@ -60,7 +62,7 @@ class PodcastMediaRepository extends Repository
|
|||
$ext = pathinfo($originalPath, PATHINFO_EXTENSION);
|
||||
$path = $podcast->getId() . '/' . $episode->getId() . '.' . $ext;
|
||||
|
||||
$podcastMedia = new PodcastMedia($storageLocation);
|
||||
$podcastMedia = new Entity\PodcastMedia($storageLocation);
|
||||
$podcastMedia->setPath($path);
|
||||
$podcastMedia->setOriginalName(basename($originalPath));
|
||||
|
||||
|
@ -89,7 +91,7 @@ class PodcastMediaRepository extends Repository
|
|||
}
|
||||
|
||||
public function delete(
|
||||
PodcastMedia $media,
|
||||
Entity\PodcastMedia $media,
|
||||
?ExtendedFilesystemInterface $fs = null
|
||||
): void {
|
||||
$fs ??= $media->getStorageLocation()->getFilesystem();
|
||||
|
|
|
@ -13,6 +13,9 @@ use Psr\Log\LoggerInterface;
|
|||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\Settings>
|
||||
*/
|
||||
class SettingsRepository extends Repository
|
||||
{
|
||||
protected ValidatorInterface $validator;
|
||||
|
|
|
@ -12,6 +12,9 @@ use Carbon\CarbonImmutable;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\SongHistory>
|
||||
*/
|
||||
class SongHistoryRepository extends Repository
|
||||
{
|
||||
protected ListenerRepository $listenerRepository;
|
||||
|
|
|
@ -25,6 +25,9 @@ use const JSON_PRETTY_PRINT;
|
|||
use const JSON_THROW_ON_ERROR;
|
||||
use const JSON_UNESCAPED_SLASHES;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\StationMedia>
|
||||
*/
|
||||
class StationMediaRepository extends Repository
|
||||
{
|
||||
public function __construct(
|
||||
|
|
|
@ -7,6 +7,9 @@ namespace App\Entity\Repository;
|
|||
use App\Doctrine\Repository;
|
||||
use App\Entity;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\StationPlaylistFolder>
|
||||
*/
|
||||
class StationPlaylistFolderRepository extends Repository
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -17,6 +17,9 @@ use Psr\Log\LoggerInterface;
|
|||
use RuntimeException;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\StationPlaylistMedia>
|
||||
*/
|
||||
class StationPlaylistMediaRepository extends Repository
|
||||
{
|
||||
protected StationQueueRepository $queueRepo;
|
||||
|
|
|
@ -11,6 +11,9 @@ use Carbon\CarbonInterface;
|
|||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\StationQueue>
|
||||
*/
|
||||
class StationQueueRepository extends Repository
|
||||
{
|
||||
public function clearForMediaAndPlaylist(Entity\StationMedia $media, Entity\StationPlaylist $playlist): void
|
||||
|
|
|
@ -7,6 +7,9 @@ namespace App\Entity\Repository;
|
|||
use App\Doctrine\Repository;
|
||||
use App\Entity;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\StationRemote>
|
||||
*/
|
||||
class StationRemoteRepository extends Repository
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -16,6 +16,9 @@ use Carbon\CarbonInterface;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\StationRequest>
|
||||
*/
|
||||
class StationRequestRepository extends Repository
|
||||
{
|
||||
protected StationMediaRepository $mediaRepo;
|
||||
|
@ -36,6 +39,29 @@ class StationRequestRepository extends Repository
|
|||
$this->deviceDetector = $deviceDetector;
|
||||
}
|
||||
|
||||
public function getPendingRequest(int $id, Entity\Station $station): ?Entity\StationRequest
|
||||
{
|
||||
return $this->repository->findOneBy(
|
||||
[
|
||||
'id' => $id,
|
||||
'station' => $station,
|
||||
'played_at' => 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function clearPendingRequests(Entity\Station $station): void
|
||||
{
|
||||
$this->em->createQuery(
|
||||
<<<'DQL'
|
||||
DELETE FROM App\Entity\StationRequest sr
|
||||
WHERE sr.station = :station
|
||||
AND sr.played_at = 0
|
||||
DQL
|
||||
)->setParameter('station', $station)
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function submit(
|
||||
Entity\Station $station,
|
||||
string $trackId,
|
||||
|
|
|
@ -14,6 +14,9 @@ use App\Radio\AutoDJ\Scheduler;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\StationStreamer>
|
||||
*/
|
||||
class StationStreamerRepository extends Repository
|
||||
{
|
||||
protected Scheduler $scheduler;
|
||||
|
|
|
@ -8,6 +8,9 @@ use App\Doctrine\Repository;
|
|||
use App\Entity;
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* @extends Repository<Entity\UnprocessableMedia>
|
||||
*/
|
||||
class UnprocessableMediaRepository extends Repository
|
||||
{
|
||||
public function findByPath(string $path, Entity\StorageLocation $storageLocation): ?Entity\UnprocessableMedia
|
||||
|
|
|
@ -7,6 +7,9 @@ namespace App\Entity\Repository;
|
|||
use App\Entity;
|
||||
use App\Security\SplitToken;
|
||||
|
||||
/**
|
||||
* @extends AbstractSplitTokenRepository<Entity\UserLoginToken>
|
||||
*/
|
||||
class UserLoginTokenRepository extends AbstractSplitTokenRepository
|
||||
{
|
||||
public function createToken(Entity\User $user): SplitToken
|
||||
|
|
|
@ -6,7 +6,6 @@ $this->layout('main', ['title' => __('Upcoming Song Queue'), 'manual' => true]);
|
|||
$props = [
|
||||
'listUrl' => (string)$router->fromHere('api:stations:queue'),
|
||||
'clearUrl' => (string)$router->fromHere('api:stations:queue:clear'),
|
||||
'locale' => substr($customization->getLocale(), 0, 2),
|
||||
'stationTimeZone' => $stationTz,
|
||||
];
|
||||
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
$(function() {
|
||||
var grid = $(".data-table").bootgrid({
|
||||
caseSensitive: false,
|
||||
sorting: false
|
||||
}).on("loaded.rs.jquery.bootgrid", function() {
|
||||
/* Executes after data is loaded and rendered */
|
||||
grid.find("time[data-original]").each(function() {
|
||||
$(this).text(moment.unix($(this).data('original')).format('lll'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,71 +1,16 @@
|
|||
<?php $this->layout('main', ['title' => __('Song Requests'), 'manual' => true]) ?>
|
||||
|
||||
<?php
|
||||
|
||||
$this->layout('main', ['title' => __('Song Requests'), 'manual' => true]);
|
||||
|
||||
/** @var App\Http\RouterInterface $router */
|
||||
$props = [
|
||||
'listUrl' => (string)$router->fromHere('api:stations:reports:requests'),
|
||||
'clearUrl' => (string)$router->fromHere('api:stations:reports:requests:clear'),
|
||||
'stationTimeZone' => $stationTz,
|
||||
];
|
||||
|
||||
/** @var \App\Assets $assets */
|
||||
$assets
|
||||
->load('bootgrid')
|
||||
->load('moment')
|
||||
->addInlineJs($this->fetch('stations/reports/requests.js'), 99);
|
||||
$assets->addVueRender('Vue_StationsReportsRequests', '#station-report-requests', $props);
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title"><?=__('Song Requests')?></h2>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-outline-danger" role="button" data-confirm-title="<?=$this->e(__('Clear all pending requests?'))?>"
|
||||
href="<?=$router->fromHere('stations:reports:requests:clear', ['csrf' => $csrf])?>">
|
||||
<i class="material-icons" aria-hidden="true">remove</i>
|
||||
<?=__('Clear Pending Requests')?>
|
||||
</a>
|
||||
</div>
|
||||
<table class="data-table table-responsive-md table table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20%;" data-column-id="timestamp"><?=__('Date Requested')?></th>
|
||||
<th style="width: 20%;" data-column-id="played_at"><?=__('Date Played')?></th>
|
||||
<th style="width: 30%;" data-column-id="song"><?=__('Song Title')?></th>
|
||||
<th style="width: 15%;" data-column-id="ip"><?=__('Requester IP')?></th>
|
||||
<th style="width: 15%;" data-column-id="actions"><?=__('Actions')?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $request_row): ?>
|
||||
<tr class="align-middle" id="request_<?=$request_row['id']?>">
|
||||
<td>
|
||||
<time data-original="<?=(int)$request_row['timestamp']?>"><?=date('F j, Y g:ia',
|
||||
$request_row['timestamp'])?></time>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($request_row['played_at'] > 0): ?>
|
||||
<time data-original="<?=(int)$request_row['played_at']?>"><?=date('F j, Y g:ia',
|
||||
$request_row['played_at'])?></time>
|
||||
<?php else: ?>
|
||||
<?=__('Not Played')?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($request_row['track']['title']): ?>
|
||||
<b><?=$this->e($request_row['track']['title'])?></b><br>
|
||||
<?=$this->e($request_row['track']['artist'])?>
|
||||
<?php else: ?>
|
||||
<?=$this->e($request_row['track']['text'])?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?=$this->e($request_row['ip'])?></td>
|
||||
<td>
|
||||
<?php if ($request_row['played_at'] == 0): ?>
|
||||
<a class="btn btn-sm btn-danger" data-confirm-title="<?=$this->e(__('Delete request?'))?>"
|
||||
href="<?=$router->fromHere('stations:reports:requests:delete',
|
||||
['request_id' => $request_row['id'], 'csrf' => $csrf])?>">
|
||||
<?=__('Delete')?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="station-report-requests"></div>
|
||||
|
|
Loading…
Reference in New Issue