test
This commit is contained in:
1 parent
72a99a5bcd
commit
75756ac6ec
10 files changed
+1144
-32
No files matched your search
@@ -37,12 +37,16 @@ class AlbumSearchService
|
||||
/**
|
||||
* Runs the end-to-end search flow and returns review stats plus saved IDs.
|
||||
*/
|
||||
public function search(AlbumSearchCriteria $criteria, bool $preferFastExternal = false): AlbumSearchResult
|
||||
public function search(
|
||||
AlbumSearchCriteria $criteria,
|
||||
bool $preferFastExternal = false,
|
||||
bool $hydrateExternalImages = true
|
||||
): AlbumSearchResult
|
||||
{
|
||||
$searchStartedAt = microtime(true);
|
||||
$externalQuery = $this->buildExternalSearchQuery($criteria);
|
||||
$hasUserFilters = $this->hasUserFilters($criteria, $externalQuery);
|
||||
$this->resetSearchProfile($criteria, $externalQuery, $preferFastExternal);
|
||||
$this->resetSearchProfile($criteria, $externalQuery, $preferFastExternal, $hydrateExternalImages);
|
||||
|
||||
$shouldQueryExternal = $criteria->shouldUseExternal()
|
||||
&& (
|
||||
@@ -60,7 +64,12 @@ class AlbumSearchService
|
||||
|
||||
if ($shouldQueryExternal) {
|
||||
$externalStartedAt = microtime(true);
|
||||
$externalData = $this->resolveExternalAlbums($criteria, $externalQuery, $preferFastExternal);
|
||||
$externalData = $this->resolveExternalAlbums(
|
||||
$criteria,
|
||||
$externalQuery,
|
||||
$preferFastExternal,
|
||||
$hydrateExternalImages
|
||||
);
|
||||
$externalPayloads = $externalData['payloads'];
|
||||
$stats = $this->mergeStats($stats, $externalData['stats']);
|
||||
$savedIds = $this->mergeSavedIds($savedIds, $externalData['savedIds']);
|
||||
@@ -105,9 +114,13 @@ class AlbumSearchService
|
||||
|
||||
private function buildExternalSearchQuery(AlbumSearchCriteria $criteria): string
|
||||
{
|
||||
if ($this->externalClient->getProviderName() === 'musicbrainz') {
|
||||
$providerName = $this->externalClient->getProviderName();
|
||||
if ($providerName === 'musicbrainz') {
|
||||
return $this->buildMusicBrainzSearchQuery($criteria);
|
||||
}
|
||||
if ($providerName === 'tidal') {
|
||||
return $this->buildTidalSearchQuery($criteria);
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if ($criteria->query !== '') {
|
||||
@@ -134,6 +147,37 @@ class AlbumSearchService
|
||||
return trim(implode(' ', $parts));
|
||||
}
|
||||
|
||||
private function buildTidalSearchQuery(AlbumSearchCriteria $criteria): string
|
||||
{
|
||||
$freeText = trim($criteria->query);
|
||||
if ($freeText !== '') {
|
||||
// Keep TIDAL search rooted in the main free-text query.
|
||||
return $freeText;
|
||||
}
|
||||
|
||||
$fallbackParts = [];
|
||||
if ($criteria->albumName !== '') {
|
||||
$fallbackParts[] = $criteria->albumName;
|
||||
}
|
||||
if ($criteria->artist !== '') {
|
||||
$fallbackParts[] = $criteria->artist;
|
||||
}
|
||||
if ($fallbackParts !== []) {
|
||||
return trim(implode(' ', $fallbackParts));
|
||||
}
|
||||
if ($criteria->genre !== '') {
|
||||
return $criteria->genre;
|
||||
}
|
||||
if ($criteria->yearFrom !== null) {
|
||||
return (string) $criteria->yearFrom;
|
||||
}
|
||||
if ($criteria->yearTo !== null) {
|
||||
return (string) $criteria->yearTo;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function buildMusicBrainzSearchQuery(AlbumSearchCriteria $criteria): string
|
||||
{
|
||||
$clauses = [];
|
||||
@@ -198,7 +242,12 @@ class AlbumSearchService
|
||||
/**
|
||||
* @return array{payloads:array<int,array<mixed>>,stats:array<string,array{count:int,avg:float}>,savedIds:array<int,string>}
|
||||
*/
|
||||
private function resolveExternalAlbums(AlbumSearchCriteria $criteria, string $externalQuery, bool $preferFastExternal): array
|
||||
private function resolveExternalAlbums(
|
||||
AlbumSearchCriteria $criteria,
|
||||
string $externalQuery,
|
||||
bool $preferFastExternal,
|
||||
bool $hydrateExternalImages
|
||||
): array
|
||||
{
|
||||
$providerName = $this->externalClient->getProviderName();
|
||||
$skipGenrePostFilter = $providerName === 'musicbrainz' && $criteria->genre !== '';
|
||||
@@ -217,6 +266,7 @@ class AlbumSearchService
|
||||
|
||||
$storedPayloads = array_map(static fn(Album $album) => $album->toTemplateArray(), $storedExternalAlbums);
|
||||
$storedPayloads = $this->filterPayloadsByTrackCount($storedPayloads);
|
||||
$storedPayloads = $this->filterPayloadsByAlbumName($storedPayloads, $criteria->albumName);
|
||||
$storedPayloads = $this->filterPayloadsByArtist($storedPayloads, $criteria->artist);
|
||||
if (!$skipGenrePostFilter) {
|
||||
$storedPayloads = $this->filterPayloadsByGenre($storedPayloads, $criteria->genre);
|
||||
@@ -231,7 +281,9 @@ class AlbumSearchService
|
||||
if (!$shouldFetchFromExternal) {
|
||||
$limitedStoredPayloads = array_slice($storedPayloads, 0, $criteria->limit);
|
||||
return [
|
||||
'payloads' => $this->hydrateExternalImages($limitedStoredPayloads),
|
||||
'payloads' => $hydrateExternalImages
|
||||
? $this->hydrateExternalImages($limitedStoredPayloads)
|
||||
: $limitedStoredPayloads,
|
||||
'stats' => $stats,
|
||||
'savedIds' => $storedIds,
|
||||
];
|
||||
@@ -239,9 +291,10 @@ class AlbumSearchService
|
||||
|
||||
$useSearchOnlyFastPath = $preferFastExternal && !$this->requiresDetailedExternalFetch($criteria);
|
||||
$apiPayloads = $useSearchOnlyFastPath
|
||||
? $this->fetchExternalPayloadsSearchOnly($criteria, $externalQuery)
|
||||
: $this->fetchExternalPayloads($criteria, $externalQuery);
|
||||
$filteredApiPayloads = $this->filterPayloadsByArtist($apiPayloads['payloads'], $criteria->artist);
|
||||
? $this->fetchExternalPayloadsSearchOnly($criteria, $externalQuery, $skipGenrePostFilter)
|
||||
: $this->fetchExternalPayloads($criteria, $externalQuery, $skipGenrePostFilter);
|
||||
$filteredApiPayloads = $this->filterPayloadsByAlbumName($apiPayloads['payloads'], $criteria->albumName);
|
||||
$filteredApiPayloads = $this->filterPayloadsByArtist($filteredApiPayloads, $criteria->artist);
|
||||
if (!$skipGenrePostFilter) {
|
||||
$filteredApiPayloads = $this->filterPayloadsByGenre($filteredApiPayloads, $criteria->genre);
|
||||
}
|
||||
@@ -249,7 +302,7 @@ class AlbumSearchService
|
||||
$mergedPayloads = $this->mergePayloadLists($filteredApiPayloads, $storedPayloads, $criteria->limit);
|
||||
|
||||
return [
|
||||
'payloads' => $this->hydrateExternalImages($mergedPayloads),
|
||||
'payloads' => $hydrateExternalImages ? $this->hydrateExternalImages($mergedPayloads) : $mergedPayloads,
|
||||
'stats' => $this->mergeStats($stats, $apiPayloads['stats']),
|
||||
'savedIds' => $this->mergeSavedIds($storedIds, $apiPayloads['savedIds']),
|
||||
];
|
||||
@@ -260,7 +313,11 @@ class AlbumSearchService
|
||||
*
|
||||
* @return array{payloads:array<int,array<mixed>>,stats:array<string,array{count:int,avg:float}>,savedIds:array<int,string>}
|
||||
*/
|
||||
private function fetchExternalPayloadsSearchOnly(AlbumSearchCriteria $criteria, string $externalQuery): array
|
||||
private function fetchExternalPayloadsSearchOnly(
|
||||
AlbumSearchCriteria $criteria,
|
||||
string $externalQuery,
|
||||
bool $skipGenreFilter = false
|
||||
): array
|
||||
{
|
||||
$perPage = max($criteria->limit, 10);
|
||||
$seenIds = [];
|
||||
@@ -286,6 +343,9 @@ class AlbumSearchService
|
||||
if ($id === '' || isset($seenIds[$id])) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->payloadMatchesCriteria($searchItem, $criteria, $skipGenreFilter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seenIds[$id] = true;
|
||||
$payloads[] = $searchItem;
|
||||
@@ -308,10 +368,14 @@ class AlbumSearchService
|
||||
/**
|
||||
* @return array{payloads:array<int,array<mixed>>,stats:array<string,array{count:int,avg:float}>,savedIds:array<int,string>}
|
||||
*/
|
||||
private function fetchExternalPayloads(AlbumSearchCriteria $criteria, string $externalQuery): array
|
||||
private function fetchExternalPayloads(
|
||||
AlbumSearchCriteria $criteria,
|
||||
string $externalQuery,
|
||||
bool $skipGenreFilter = false
|
||||
): array
|
||||
{
|
||||
if (!$this->requiresDetailedExternalFetch($criteria)) {
|
||||
return $this->fetchExternalPayloadsLight($criteria, $externalQuery);
|
||||
return $this->fetchExternalPayloadsLight($criteria, $externalQuery, $skipGenreFilter);
|
||||
}
|
||||
|
||||
$perPage = max($criteria->limit, 10);
|
||||
@@ -363,6 +427,9 @@ class AlbumSearchService
|
||||
if ($albumId === '' || !$this->isIngestableExternalAlbum((array) $payload)) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->payloadMatchesCriteria((array) $payload, $criteria, $skipGenreFilter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->albumRepository->upsertFromExternalAlbum(
|
||||
(array) $payload,
|
||||
@@ -427,7 +494,11 @@ class AlbumSearchService
|
||||
*
|
||||
* @return array{payloads:array<int,array<mixed>>,stats:array<string,array{count:int,avg:float}>,savedIds:array<int,string>}
|
||||
*/
|
||||
private function fetchExternalPayloadsLight(AlbumSearchCriteria $criteria, string $externalQuery): array
|
||||
private function fetchExternalPayloadsLight(
|
||||
AlbumSearchCriteria $criteria,
|
||||
string $externalQuery,
|
||||
bool $skipGenreFilter = false
|
||||
): array
|
||||
{
|
||||
$perPage = max($criteria->limit, 10);
|
||||
$maxCandidateIdsPerPage = max($criteria->limit + self::VALIDATION_ID_HEADROOM, $criteria->limit);
|
||||
@@ -496,6 +567,9 @@ class AlbumSearchService
|
||||
if ($externalId === '') {
|
||||
continue;
|
||||
}
|
||||
if (!$this->payloadMatchesCriteria($albumPayload, $criteria, $skipGenreFilter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->albumRepository->upsertFromExternalAlbum(
|
||||
$albumPayload,
|
||||
@@ -786,6 +860,23 @@ class AlbumSearchService
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<mixed>> $payloads
|
||||
* @return array<int,array<mixed>>
|
||||
*/
|
||||
private function filterPayloadsByAlbumName(array $payloads, string $albumNeedle): array
|
||||
{
|
||||
$needle = trim(mb_strtolower($albumNeedle));
|
||||
if ($needle === '') {
|
||||
return $payloads;
|
||||
}
|
||||
|
||||
return array_values(array_filter($payloads, static function (array $payload) use ($needle): bool {
|
||||
$name = mb_strtolower(trim((string) ($payload['name'] ?? '')));
|
||||
return $name !== '' && str_contains($name, $needle);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<mixed>> $payloads
|
||||
* @return array<int,array<mixed>>
|
||||
@@ -855,6 +946,22 @@ class AlbumSearchService
|
||||
return array_values(array_filter($payloads, fn(array $payload): bool => $this->hasAtLeastOneTrack($payload)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
private function payloadMatchesCriteria(array $payload, AlbumSearchCriteria $criteria, bool $skipGenreFilter): bool
|
||||
{
|
||||
$candidates = [$payload];
|
||||
$candidates = $this->filterPayloadsByAlbumName($candidates, $criteria->albumName);
|
||||
$candidates = $this->filterPayloadsByArtist($candidates, $criteria->artist);
|
||||
if (!$skipGenreFilter) {
|
||||
$candidates = $this->filterPayloadsByGenre($candidates, $criteria->genre);
|
||||
}
|
||||
$candidates = $this->filterPayloadsByYear($candidates, $criteria->yearFrom, $criteria->yearTo);
|
||||
|
||||
return $candidates !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<mixed>> $payloads
|
||||
* @return array<int,array<mixed>>
|
||||
@@ -936,7 +1043,12 @@ class AlbumSearchService
|
||||
|| $criteria->yearTo !== null;
|
||||
}
|
||||
|
||||
private function resetSearchProfile(AlbumSearchCriteria $criteria, string $externalQuery, bool $preferFastExternal): void
|
||||
private function resetSearchProfile(
|
||||
AlbumSearchCriteria $criteria,
|
||||
string $externalQuery,
|
||||
bool $preferFastExternal,
|
||||
bool $hydrateExternalImages
|
||||
): void
|
||||
{
|
||||
$this->lastSearchProfile = [
|
||||
'query' => $externalQuery,
|
||||
@@ -944,6 +1056,7 @@ class AlbumSearchService
|
||||
'limit' => $criteria->limit,
|
||||
'external_provider' => $this->externalClient->getProviderName(),
|
||||
'prefer_fast_external' => $preferFastExternal,
|
||||
'image_hydration_enabled' => $hydrateExternalImages,
|
||||
'timings_ms' => [
|
||||
'total' => 0.0,
|
||||
'external' => 0.0,
|
||||
|
||||
Reference in new issue
Block a user