getStationIds() as $stationId) { $filterableAttributes[] = 'station_' . $stationId . '_playlists'; $filterableAttributes[] = 'station_' . $stationId . '_is_requestable'; $filterableAttributes[] = 'station_' . $stationId . '_is_on_demand'; } foreach ($this->customFieldRepo->getFieldIds() as $fieldId => $fieldShortCode) { $mediaFields[] = 'custom_field_' . $fieldId; } $indexSettings = [ 'filterableAttributes' => $filterableAttributes, 'sortableAttributes' => $mediaFields, 'displayedAttributes' => $this->environment->isProduction() ? ['id'] : ['*'], ]; // Avoid updating settings unless necessary to avoid triggering a reindex. try { $this->indexClient->fetchRawInfo(); } catch (ApiException) { $response = $this->indexClient->create( $this->indexClient->getUid() ?? '', ['primaryKey' => 'id'] ); $this->indexClient->waitForTask($response['taskUid']); } $currentSettings = $this->indexClient->getSettings(); $settingsToUpdate = []; foreach ($indexSettings as $settingKey => $setting) { $currentSetting = $currentSettings[$settingKey] ?? []; sort($setting); if ($currentSetting !== $setting) { $settingsToUpdate[$settingKey] = $setting; } } if (!empty($settingsToUpdate)) { $response = $this->indexClient->updateSettings($settingsToUpdate); $this->indexClient->waitForTask($response['taskUid']); } } public function getIdsInIndex(): array { $ids = []; foreach ($this->getAllDocuments(['id', 'mtime']) as $document) { $ids[$document['id']] = $document['mtime']; } return $ids; } public function getAllDocuments(array $fields = ['*']): iterable { $perPage = Meilisearch::BATCH_SIZE; $documentsQuery = (new DocumentsQuery()) ->setOffset(0) ->setLimit($perPage) ->setFields($fields); $documents = $this->indexClient->getDocuments($documentsQuery); yield from $documents->getIterator(); if ($documents->getTotal() <= $perPage) { return; } $totalPages = ceil($documents->getTotal() / $perPage); for ($page = 1; $page <= $totalPages; $page++) { $documentsQuery->setOffset($page * $perPage); $documents = $this->indexClient->getDocuments($documentsQuery); yield from $documents->getIterator(); } } public function deleteIds(array $ids): void { $this->indexClient->deleteDocuments($ids); } public function refreshMedia( array $ids, bool $includePlaylists = false ): void { if ($includePlaylists) { $mediaPlaylistsRaw = $this->em->createQuery( <<<'DQL' SELECT spm.media_id, spm.playlist_id FROM App\Entity\StationPlaylistMedia spm WHERE spm.media_id IN (:mediaIds) DQL )->setParameter('mediaIds', $ids) ->getArrayResult(); $mediaPlaylists = []; $playlistIds = []; foreach ($mediaPlaylistsRaw as $mediaPlaylistRow) { $mediaId = $mediaPlaylistRow['media_id']; $playlistId = $mediaPlaylistRow['playlist_id']; $playlistIds[$playlistId] = $playlistId; $mediaPlaylists[$mediaId] ??= []; $mediaPlaylists[$mediaId][] = $playlistId; } $stationIds = $this->getStationIds(); $playlistsRaw = $this->em->createQuery( <<<'DQL' SELECT p.id, p.station_id, p.include_in_on_demand, p.include_in_requests FROM App\Entity\StationPlaylist p WHERE p.id IN (:playlistIds) AND p.station_id IN (:stationIds) AND p.is_enabled = 1 DQL )->setParameter('playlistIds', $playlistIds) ->setParameter('stationIds', $stationIds) ->getArrayResult(); $playlists = []; foreach ($playlistsRaw as $playlistRow) { $playlists[$playlistRow['id']] = $playlistRow; } } $customFieldsRaw = $this->em->createQuery( <<<'DQL' SELECT smcf.media_id, smcf.field_id, smcf.value FROM App\Entity\StationMediaCustomField smcf WHERE smcf.media_id IN (:mediaIds) DQL )->setParameter('mediaIds', $ids) ->getArrayResult(); $customFields = []; foreach ($customFieldsRaw as $customFieldRow) { $mediaId = $customFieldRow['media_id']; $customFields[$mediaId] ??= []; $customFields[$mediaId]['custom_field_' . $customFieldRow['field_id']] = $customFieldRow['value']; } $mediaRaw = $this->em->createQuery( <<<'DQL' SELECT sm.id, sm.path, sm.mtime, sm.length_text, sm.title, sm.artist, sm.album, sm.genre, sm.isrc FROM App\Entity\StationMedia sm WHERE sm.storage_location = :storageLocation AND sm.id IN (:ids) DQL )->setParameter('storageLocation', $this->storageLocation) ->setParameter('ids', $ids) ->toIterable([], AbstractQuery::HYDRATE_ARRAY); $media = []; foreach ($mediaRaw as $row) { $mediaId = $row['id']; $record = [ 'id' => $row['id'], 'path' => $row['path'], 'mtime' => $row['mtime'], 'duration' => $row['length_text'], 'title' => $row['title'], 'artist' => $row['artist'], 'album' => $row['album'], 'genre' => $row['genre'], 'isrc' => $row['isrc'], ]; if (isset($customFields[$mediaId])) { $record = array_merge($record, $customFields[$mediaId]); } if ($includePlaylists) { foreach ($stationIds as $stationId) { $record['station_' . $stationId . '_playlists'] = []; $record['station_' . $stationId . '_is_requestable'] = false; $record['station_' . $stationId . '_is_on_demand'] = false; } if (isset($mediaPlaylists[$mediaId])) { foreach ($mediaPlaylists[$mediaId] as $mediaPlaylistId) { if (!isset($playlists[$mediaPlaylistId])) { continue; } $playlist = $playlists[$mediaPlaylistId]; $stationId = $playlist['station_id']; $record['station_' . $stationId . '_playlists'][] = $mediaPlaylistId; if ($playlist['include_in_requests']) { $record['station_' . $stationId . '_is_requestable'] = true; } if ($playlist['include_in_on_demand']) { $record['station_' . $stationId . '_is_on_demand'] = true; } } } } $media[$mediaId] = $record; } if ($includePlaylists) { $this->indexClient->addDocumentsInBatches( $media, Meilisearch::BATCH_SIZE ); } else { $this->indexClient->updateDocumentsInBatches( $media, Meilisearch::BATCH_SIZE ); } } public function refreshPlaylists( Station $station, ?array $ids = null ): void { $stationId = $station->getIdRequired(); $playlistsKey = 'station_' . $stationId . '_playlists'; $isRequestableKey = 'station_' . $stationId . '_is_requestable'; $isOnDemandKey = 'station_' . $stationId . '_is_on_demand'; $media = []; if (null === $ids) { $allMediaRaw = $this->em->createQuery( <<<'DQL' SELECT m.id FROM App\Entity\StationMedia m WHERE m.storage_location = :storageLocation DQL )->setParameter('storageLocation', $this->storageLocation) ->toIterable([], AbstractQuery::HYDRATE_ARRAY); foreach ($allMediaRaw as $mediaRow) { $media[$mediaRow['id']] = [ 'id' => $mediaRow['id'], $playlistsKey => [], $isRequestableKey => false, $isOnDemandKey => false, ]; } } else { foreach ($ids as $mediaId) { $media[$mediaId] = [ 'id' => $mediaId, $playlistsKey => [], $isRequestableKey => false, $isOnDemandKey => false, ]; } } $allPlaylists = $this->em->createQuery( <<<'DQL' SELECT p.id, p.include_in_on_demand, p.include_in_requests FROM App\Entity\StationPlaylist p WHERE p.station = :station AND p.is_enabled = 1 DQL )->setParameter('station', $station) ->getArrayResult(); $allPlaylistIds = []; $onDemandPlaylists = []; $requestablePlaylists = []; foreach ($allPlaylists as $playlist) { $allPlaylistIds[] = $playlist['id']; if ($playlist['include_in_on_demand']) { $onDemandPlaylists[$playlist['id']] = $playlist['id']; } if ($playlist['include_in_requests']) { $requestablePlaylists[$playlist['id']] = $playlist['id']; } } if (null === $ids) { $mediaInPlaylists = $this->em->createQuery( <<<'DQL' SELECT spm.media_id, spm.playlist_id FROM App\Entity\StationPlaylistMedia spm WHERE spm.playlist_id IN (:allPlaylistIds) DQL )->setParameter('allPlaylistIds', $allPlaylistIds) ->toIterable([], AbstractQuery::HYDRATE_ARRAY); } else { $mediaInPlaylists = $this->em->createQuery( <<<'DQL' SELECT spm.media_id, spm.playlist_id FROM App\Entity\StationPlaylistMedia spm WHERE spm.playlist_id IN (:allPlaylistIds) AND spm.media_id IN (:mediaIds) DQL )->setParameter('allPlaylistIds', $allPlaylistIds) ->setParameter('mediaIds', $ids) ->toIterable([], AbstractQuery::HYDRATE_ARRAY); } foreach ($mediaInPlaylists as $spmRow) { $mediaId = $spmRow['media_id']; $playlistId = $spmRow['playlist_id']; $media[$mediaId][$playlistsKey][] = $playlistId; if (isset($requestablePlaylists[$playlistId])) { $media[$mediaId][$isRequestableKey] = true; } if (isset($onDemandPlaylists[$playlistId])) { $media[$mediaId][$isOnDemandKey] = true; } } $this->indexClient->updateDocumentsInBatches( array_values($media), Meilisearch::BATCH_SIZE ); } /** * @return PaginatorAdapter */ public function getRequestableSearchPaginator( Station $station, callable $hydrateCallback, ?string $query, array $searchParams = [], array $options = [], ): PaginatorAdapter { return $this->getSearchPaginator( $hydrateCallback, $query, [ ...$searchParams, 'filter' => [ [ 'station_' . $station->getIdRequired() . '_is_requestable = true', ], ], ], $options ); } /** * @return PaginatorAdapter */ public function getOnDemandSearchPaginator( Station $station, callable $hydrateCallback, ?string $query, array $searchParams = [], array $options = [], ): PaginatorAdapter { return $this->getSearchPaginator( $hydrateCallback, $query, [ ...$searchParams, 'filter' => [ [ 'station_' . $station->getIdRequired() . '_is_on_demand = true', ], ], ], $options ); } /** * @return PaginatorAdapter */ public function getSearchPaginator( callable $hydrateCallback, ?string $query, array $searchParams = [], array $options = [], ): PaginatorAdapter { return new PaginatorAdapter( $this->indexClient, $hydrateCallback(...), $query, $searchParams, $options, ); } public function searchMedia( string $query, ?StationPlaylist $playlist = null ): array { $searchParams = [ 'hitsPerPage' => PHP_INT_MAX, 'page' => 1, ]; if (null !== $playlist) { $station = $playlist->getStation(); $searchParams['filter'] = [ [ 'station_' . $station->getIdRequired() . '_playlists = ' . $playlist->getIdRequired(), ], ]; } /** @var SearchResult $searchResult */ $searchResult = $this->indexClient->search( $query, $searchParams ); return array_column($searchResult->getHits(), 'id'); } /** @return int[] */ private function getStationIds(): array { return $this->em->createQuery( <<<'DQL' SELECT s.id FROM App\Entity\Station s WHERE s.media_storage_location = :storageLocation AND s.is_enabled = 1 DQL )->setParameter('storageLocation', $this->storageLocation) ->getSingleColumnResult(); } }