From 2586277e9fa9effe35ee6bbbc51a8f61844f0edd Mon Sep 17 00:00:00 2001 From: Buster Neece Date: Sun, 29 Jan 2023 08:38:59 -0600 Subject: [PATCH] Switch Requests to use Meilisearch. --- frontend/vue/components/Public/OnDemand.vue | 2 +- frontend/vue/components/Public/Requests.vue | 25 ++-- .../Api/Stations/RequestsController.php | 116 ++++++++---------- 3 files changed, 64 insertions(+), 79 deletions(-) diff --git a/frontend/vue/components/Public/OnDemand.vue b/frontend/vue/components/Public/OnDemand.vue index 7c739d1b0..55d3e6091 100644 --- a/frontend/vue/components/Public/OnDemand.vue +++ b/frontend/vue/components/Public/OnDemand.vue @@ -32,7 +32,7 @@
{ key: 'name', isRowHeader: true, label: $gettext('Name'), - sortable: true, + sortable: false, selectable: true }, { - key: 'song.title', + key: 'title', label: $gettext('Title'), sortable: true, selectable: true, visible: false, + formatter: (value, key, item) => item.song.title }, { - key: 'song.artist', + key: 'artist', label: $gettext('Artist'), sortable: true, selectable: true, visible: false, + formatter: (value, key, item) => item.song.artist }, { - key: 'song.album', + key: 'album', label: $gettext('Album'), sortable: true, selectable: true, - visible: false + visible: false, + formatter: (value, key, item) => item.song.album }, { - key: 'song.genre', + key: 'genre', label: $gettext('Genre'), sortable: true, selectable: true, - visible: false + visible: false, + formatter: (value, key, item) => item.song.genre } ]; forEach({...props.customFields}, (field) => { fields.push({ - key: 'song.custom_fields.' + field.short_name, + key: 'custom_field_' + field.id, label: field.name, sortable: false, selectable: true, - visible: false + visible: false, + formatter: (value, key, item) => item.song.custom_fields[field.short_name] }); }); diff --git a/src/Controller/Api/Stations/RequestsController.php b/src/Controller/Api/Stations/RequestsController.php index 808d3329a..4f5f28442 100644 --- a/src/Controller/Api/Stations/RequestsController.php +++ b/src/Controller/Api/Stations/RequestsController.php @@ -10,8 +10,7 @@ use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; use App\Paginator; -use App\Radio\AutoDJ\Scheduler; -use Carbon\CarbonImmutable; +use App\Service\Meilisearch; use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -62,13 +61,13 @@ use Psr\Http\Message\ResponseInterface; ] ) ] -final class RequestsController +final readonly class RequestsController { public function __construct( - private readonly EntityManagerInterface $em, - private readonly Entity\Repository\StationRequestRepository $requestRepo, - private readonly Entity\ApiGenerator\SongApiGenerator $songApiGenerator, - private readonly Scheduler $scheduler + private EntityManagerInterface $em, + private Entity\Repository\StationRequestRepository $requestRepo, + private Entity\ApiGenerator\SongApiGenerator $songApiGenerator, + private Meilisearch $meilisearch ) { } @@ -79,60 +78,68 @@ final class RequestsController ): ResponseInterface { $station = $request->getStation(); - $playlistIds = $this->getRequestablePlaylists($station); + // Verify that the station supports on-demand streaming. + if (!$station->getEnableRequests()) { + return $response->withStatus(403) + ->withJson(new Entity\Api\Error(403, __('This station does not support requests.'))); + } - $qb = $this->em->createQueryBuilder(); - $qb->select('sm, spm, sp') - ->from(Entity\StationMedia::class, 'sm') - ->leftJoin('sm.playlists', 'spm') - ->leftJoin('spm.playlist', 'sp') - ->where('sm.storage_location = :storageLocation') - ->andWhere('sp.id IN (:playlistIds)') - ->setParameter('storageLocation', $station->getMediaStorageLocation()) - ->setParameter('playlistIds', $playlistIds); + if (!$this->meilisearch->isSupported()) { + return $response->withStatus(403) + ->withJson(new Entity\Api\Error(403, __('This feature is not supported on this installation.'))); + } + + $index = $this->meilisearch->getIndex($station->getMediaStorageLocation()); $queryParams = $request->getQueryParams(); + $searchPhrase = trim($queryParams['searchPhrase'] ?? ''); + $searchParams = []; if (!empty($queryParams['sort'])) { - $sortDirection = (($queryParams['sortOrder'] ?? 'ASC') === 'ASC') ? 'ASC' : 'DESC'; - - match ($queryParams['sort']) { - 'name', 'song_title' => $qb->addOrderBy('sm.title', $sortDirection), - 'song_artist' => $qb->addOrderBy('sm.artist', $sortDirection), - 'song_album' => $qb->addOrderBy('sm.album', $sortDirection), - 'song_genre' => $qb->addOrderBy('sm.genre', $sortDirection), - default => null, - }; - } else { - $qb->orderBy('sm.artist', 'ASC') - ->addOrderBy('sm.title', 'ASC'); + $sortField = (string)$queryParams['sort']; + $sortDirection = strtolower($queryParams['sortOrder'] ?? 'asc'); + $searchParams['sort'] = [$sortField . ':' . $sortDirection]; } - $search_phrase = trim($queryParams['searchPhrase'] ?? ''); - if (!empty($search_phrase)) { - $qb->andWhere('(sm.title LIKE :query OR sm.artist LIKE :query OR sm.album LIKE :query)') - ->setParameter('query', '%' . $search_phrase . '%'); - } + $hydrateCallback = function (array $results) { + $ids = array_column($results, 'id'); - $paginator = Paginator::fromQueryBuilder($qb, $request); + return $this->em->createQuery( + <<<'DQL' + SELECT sm + FROM App\Entity\StationMedia sm + WHERE sm.id IN (:ids) + ORDER BY FIELD(sm.id, :ids) + DQL + )->setParameter('ids', $ids) + ->toIterable(); + }; + + $paginatorAdapter = $index->getOnDemandSearchPaginator( + $station, + $hydrateCallback, + $searchPhrase, + $searchParams, + ); + + $paginator = Paginator::fromAdapter($paginatorAdapter, $request); $router = $request->getRouter(); - $baseUrl = $router->getBaseUrl(); $paginator->setPostprocessor( - function (Entity\StationMedia $media_row) use ($station, $baseUrl, $router) { + function (Entity\StationMedia $media) use ($station, $router) { $row = new Entity\Api\StationRequest(); - $row->song = ($this->songApiGenerator)($media_row, $station, $baseUrl); - $row->request_id = $media_row->getUniqueId(); + $row->song = ($this->songApiGenerator)($media, $station, $router->getBaseUrl()); + $row->request_id = $media->getUniqueId(); $row->request_url = $router->named( 'api:requests:submit', [ 'station_id' => $station->getId(), - 'media_id' => $media_row->getUniqueId(), + 'media_id' => $media->getUniqueId(), ] ); - $row->resolveUrls($baseUrl); + $row->resolveUrls($router->getBaseUrl()); return $row; } @@ -141,33 +148,6 @@ final class RequestsController return $paginator->write($response); } - /** - * @param Entity\Station $station - */ - private function getRequestablePlaylists(Entity\Station $station): array - { - $playlists = $this->em->createQuery( - <<setParameter('station', $station) - ->toIterable(); - - $ids = []; - $now = CarbonImmutable::now($station->getTimezoneObject()); - - /** @var Entity\StationPlaylist $playlist */ - foreach ($playlists as $playlist) { - if ($this->scheduler->isPlaylistScheduledToPlayNow($playlist, $now)) { - $ids[] = $playlist->getIdRequired(); - } - } - - return $ids; - } - public function submitAction( ServerRequest $request, Response $response,