eerrrrrr
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 1m57s
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 1m57s
This commit is contained in:
@@ -2,15 +2,17 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Service\SpotifyClient;
|
||||
use App\Service\ImageStorage;
|
||||
use App\Repository\AlbumRepository;
|
||||
use App\Dto\AlbumSearchCriteria;
|
||||
use App\Entity\Album;
|
||||
use App\Entity\Review;
|
||||
use App\Entity\User;
|
||||
use App\Form\ReviewType;
|
||||
use App\Form\AlbumType;
|
||||
use App\Repository\AlbumRepository;
|
||||
use App\Repository\ReviewRepository;
|
||||
use App\Service\AlbumSearchService;
|
||||
use App\Service\ImageStorage;
|
||||
use App\Service\SpotifyClient;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
@@ -19,7 +21,6 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Bundle\SecurityBundle\Attribute\IsGranted;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* AlbumController orchestrates search, CRUD, and review entry on albums.
|
||||
@@ -28,6 +29,7 @@ class AlbumController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ImageStorage $imageStorage,
|
||||
private readonly AlbumSearchService $albumSearch,
|
||||
private readonly int $searchLimit = 20
|
||||
) {
|
||||
}
|
||||
@@ -36,37 +38,21 @@ class AlbumController extends AbstractController
|
||||
* Searches Spotify plus local albums and decorates results with review stats.
|
||||
*/
|
||||
#[Route('/', name: 'album_search', methods: ['GET'])]
|
||||
public function search(Request $request, SpotifyClient $spotify, ReviewRepository $reviewRepo, AlbumRepository $albumRepo, EntityManagerInterface $em, LoggerInterface $logger): Response
|
||||
public function search(Request $request): Response
|
||||
{
|
||||
$filters = $this->buildSearchFilters($request);
|
||||
$stats = [];
|
||||
$savedIds = [];
|
||||
|
||||
$spotifyData = $this->resolveSpotifyAlbums($filters, $spotify, $albumRepo, $reviewRepo, $em, $logger);
|
||||
$stats = $this->mergeStats($stats, $spotifyData['stats']);
|
||||
$savedIds = $this->mergeSavedIds($savedIds, $spotifyData['savedIds']);
|
||||
|
||||
$userData = $this->resolveUserAlbums($filters, $albumRepo, $reviewRepo);
|
||||
$stats = $this->mergeStats($stats, $userData['stats']);
|
||||
|
||||
$albums = $this->composeAlbumList(
|
||||
$filters['source'],
|
||||
$userData['payloads'],
|
||||
$spotifyData['payloads'],
|
||||
$filters['limit']
|
||||
);
|
||||
$savedIds = $this->mergeSavedIds($savedIds, []);
|
||||
$criteria = AlbumSearchCriteria::fromRequest($request, $this->searchLimit);
|
||||
$result = $this->albumSearch->search($criteria);
|
||||
|
||||
return $this->render('album/search.html.twig', [
|
||||
'query' => $filters['query'],
|
||||
'album' => $filters['albumName'],
|
||||
'artist' => $filters['artist'],
|
||||
'year_from' => $filters['yearFrom'] ?: '',
|
||||
'year_to' => $filters['yearTo'] ?: '',
|
||||
'albums' => $albums,
|
||||
'stats' => $stats,
|
||||
'savedIds' => $savedIds,
|
||||
'source' => $filters['source'],
|
||||
'query' => $criteria->query,
|
||||
'album' => $criteria->albumName,
|
||||
'artist' => $criteria->artist,
|
||||
'year_from' => $criteria->yearFrom ?? '',
|
||||
'year_to' => $criteria->yearTo ?? '',
|
||||
'albums' => $result->albums,
|
||||
'stats' => $result->stats,
|
||||
'savedIds' => $result->savedIds,
|
||||
'source' => $criteria->source,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -291,7 +277,7 @@ class AlbumController extends AbstractController
|
||||
*/
|
||||
private function canManageAlbum(Album $album): bool
|
||||
{
|
||||
if ($this->isGranted('ROLE_ADMIN')) {
|
||||
if ($this->isGranted('ROLE_MODERATOR')) {
|
||||
return true;
|
||||
}
|
||||
return $album->getSource() === 'user' && $this->isAlbumOwner($album);
|
||||
@@ -348,309 +334,6 @@ class AlbumController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* query:string,
|
||||
* albumName:string,
|
||||
* artist:string,
|
||||
* source:string,
|
||||
* yearFrom:int,
|
||||
* yearTo:int,
|
||||
* limit:int,
|
||||
* spotifyQuery:string,
|
||||
* hasUserFilters:bool,
|
||||
* useSpotify:bool
|
||||
* }
|
||||
*/
|
||||
private function buildSearchFilters(Request $request): array
|
||||
{
|
||||
$query = trim((string) $request->query->get('q', ''));
|
||||
$albumName = trim($request->query->getString('album', ''));
|
||||
$artist = trim($request->query->getString('artist', ''));
|
||||
$source = $request->query->getString('source', 'all');
|
||||
$yearFromRaw = trim((string) $request->query->get('year_from', ''));
|
||||
$yearToRaw = trim((string) $request->query->get('year_to', ''));
|
||||
$yearFrom = (preg_match('/^\d{4}$/', $yearFromRaw)) ? (int) $yearFromRaw : 0;
|
||||
$yearTo = (preg_match('/^\d{4}$/', $yearToRaw)) ? (int) $yearToRaw : 0;
|
||||
$spotifyQuery = $this->buildSpotifyQuery($query, $albumName, $artist, $yearFrom, $yearTo);
|
||||
$hasUserFilters = ($spotifyQuery !== '' || $albumName !== '' || $artist !== '' || $yearFrom > 0 || $yearTo > 0);
|
||||
$useSpotify = ($source === 'all' || $source === 'spotify');
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'albumName' => $albumName,
|
||||
'artist' => $artist,
|
||||
'source' => $source,
|
||||
'yearFrom' => $yearFrom,
|
||||
'yearTo' => $yearTo,
|
||||
'limit' => $this->searchLimit,
|
||||
'spotifyQuery' => $spotifyQuery,
|
||||
'hasUserFilters' => $hasUserFilters,
|
||||
'useSpotify' => $useSpotify,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildSpotifyQuery(string $query, string $albumName, string $artist, int $yearFrom, int $yearTo): string
|
||||
{
|
||||
$parts = [];
|
||||
if ($albumName !== '') { $parts[] = 'album:' . $albumName; }
|
||||
if ($artist !== '') { $parts[] = 'artist:' . $artist; }
|
||||
if ($yearFrom > 0 || $yearTo > 0) {
|
||||
if ($yearFrom > 0 && $yearTo > 0 && $yearTo >= $yearFrom) {
|
||||
$parts[] = 'year:' . $yearFrom . '-' . $yearTo;
|
||||
} else {
|
||||
$y = $yearFrom > 0 ? $yearFrom : $yearTo;
|
||||
$parts[] = 'year:' . $y;
|
||||
}
|
||||
}
|
||||
if ($query !== '') { $parts[] = $query; }
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{payloads:array<int,array>,stats:array<string,array{count:int,avg:float}>,savedIds:array<int,string>}
|
||||
*/
|
||||
private function resolveSpotifyAlbums(
|
||||
array $filters,
|
||||
SpotifyClient $spotify,
|
||||
AlbumRepository $albumRepo,
|
||||
ReviewRepository $reviewRepo,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger
|
||||
): array {
|
||||
if (!$filters['useSpotify'] || $filters['spotifyQuery'] === '') {
|
||||
return ['payloads' => [], 'stats' => [], 'savedIds' => []];
|
||||
}
|
||||
|
||||
$stored = $albumRepo->searchSpotifyAlbums(
|
||||
$filters['spotifyQuery'],
|
||||
$filters['albumName'],
|
||||
$filters['artist'],
|
||||
$filters['yearFrom'],
|
||||
$filters['yearTo'],
|
||||
$filters['limit']
|
||||
);
|
||||
$storedPayloads = array_map(static fn($a) => $a->toTemplateArray(), $stored);
|
||||
$storedIds = $this->collectSpotifyIds($stored);
|
||||
$stats = $storedIds ? $reviewRepo->getAggregatesForAlbumIds($storedIds) : [];
|
||||
$savedIds = $storedIds;
|
||||
|
||||
if (count($stored) >= $filters['limit']) {
|
||||
return [
|
||||
'payloads' => array_slice($storedPayloads, 0, $filters['limit']),
|
||||
'stats' => $stats,
|
||||
'savedIds' => $savedIds,
|
||||
];
|
||||
}
|
||||
|
||||
$apiPayloads = $this->fetchSpotifyPayloads(
|
||||
$filters,
|
||||
$spotify,
|
||||
$albumRepo,
|
||||
$reviewRepo,
|
||||
$em,
|
||||
$logger
|
||||
);
|
||||
|
||||
$payloads = $this->mergePayloadLists($apiPayloads['payloads'], $storedPayloads, $filters['limit']);
|
||||
$stats = $this->mergeStats($stats, $apiPayloads['stats']);
|
||||
$savedIds = $this->mergeSavedIds($savedIds, $apiPayloads['savedIds']);
|
||||
|
||||
return ['payloads' => $payloads, 'stats' => $stats, 'savedIds' => $savedIds];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{payloads:array<int,array>,stats:array<string,array{count:int,avg:float}>,savedIds:array<int,string>}
|
||||
*/
|
||||
private function fetchSpotifyPayloads(
|
||||
array $filters,
|
||||
SpotifyClient $spotify,
|
||||
AlbumRepository $albumRepo,
|
||||
ReviewRepository $reviewRepo,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger
|
||||
): array {
|
||||
$result = $spotify->searchAlbums($filters['spotifyQuery'], $filters['limit']);
|
||||
$searchItems = $result['albums']['items'] ?? [];
|
||||
$logger->info('Album search results received', ['query' => $filters['spotifyQuery'], 'items' => is_countable($searchItems) ? count($searchItems) : 0]);
|
||||
|
||||
if (!$searchItems) {
|
||||
return ['payloads' => [], 'stats' => [], 'savedIds' => []];
|
||||
}
|
||||
|
||||
$ids = $this->extractSpotifyIds($searchItems);
|
||||
if ($ids === []) {
|
||||
return ['payloads' => [], 'stats' => [], 'savedIds' => []];
|
||||
}
|
||||
|
||||
$full = $spotify->getAlbums($ids);
|
||||
$albumsPayload = is_array($full) ? ($full['albums'] ?? []) : [];
|
||||
if ($albumsPayload === [] && $searchItems !== []) {
|
||||
$albumsPayload = $searchItems;
|
||||
$logger->warning('Spotify getAlbums returned empty; falling back to search items', ['count' => count($albumsPayload)]);
|
||||
}
|
||||
|
||||
$upserted = 0;
|
||||
foreach ($albumsPayload as $sa) {
|
||||
$albumRepo->upsertFromSpotifyAlbum((array) $sa);
|
||||
$upserted++;
|
||||
}
|
||||
$em->flush();
|
||||
$logger->info('Albums upserted to DB', ['upserted' => $upserted]);
|
||||
|
||||
$existing = $albumRepo->findBySpotifyIdsKeyed($ids);
|
||||
$payloads = [];
|
||||
foreach ($ids as $sid) {
|
||||
if (isset($existing[$sid])) {
|
||||
$payloads[] = $existing[$sid]->toTemplateArray();
|
||||
}
|
||||
}
|
||||
|
||||
$stats = $reviewRepo->getAggregatesForAlbumIds($ids);
|
||||
|
||||
return [
|
||||
'payloads' => $payloads,
|
||||
'stats' => $stats,
|
||||
'savedIds' => array_keys($existing),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{payloads:array<int,array>,stats:array<string,array{count:int,avg:float}>}
|
||||
*/
|
||||
private function resolveUserAlbums(array $filters, AlbumRepository $albumRepo, ReviewRepository $reviewRepo): array
|
||||
{
|
||||
if (!$filters['hasUserFilters'] || ($filters['source'] !== 'user' && $filters['source'] !== 'all')) {
|
||||
return ['payloads' => [], 'stats' => []];
|
||||
}
|
||||
|
||||
$userAlbums = $albumRepo->searchUserAlbums(
|
||||
$filters['query'],
|
||||
$filters['albumName'],
|
||||
$filters['artist'],
|
||||
$filters['yearFrom'],
|
||||
$filters['yearTo'],
|
||||
$filters['limit']
|
||||
);
|
||||
if ($userAlbums === []) {
|
||||
return ['payloads' => [], 'stats' => []];
|
||||
}
|
||||
|
||||
$entityIds = array_values(array_map(static fn($a) => $a->getId(), $userAlbums));
|
||||
$userStats = $reviewRepo->getAggregatesForAlbumEntityIds($entityIds);
|
||||
$payloads = array_map(static fn($a) => $a->toTemplateArray(), $userAlbums);
|
||||
|
||||
return ['payloads' => $payloads, 'stats' => $this->mapUserStatsToLocalIds($userAlbums, $userStats)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Album> $userAlbums
|
||||
* @param array<int,array{count:int,avg:float}> $userStats
|
||||
* @return array<string,array{count:int,avg:float}>
|
||||
*/
|
||||
private function mapUserStatsToLocalIds(array $userAlbums, array $userStats): array
|
||||
{
|
||||
$mapped = [];
|
||||
foreach ($userAlbums as $album) {
|
||||
$entityId = (int) $album->getId();
|
||||
$localId = (string) $album->getLocalId();
|
||||
if ($localId !== '' && isset($userStats[$entityId])) {
|
||||
$mapped[$localId] = $userStats[$entityId];
|
||||
}
|
||||
}
|
||||
return $mapped;
|
||||
}
|
||||
|
||||
private function composeAlbumList(string $source, array $userPayloads, array $spotifyPayloads, int $limit): array
|
||||
{
|
||||
if ($source === 'user') {
|
||||
return array_slice($userPayloads, 0, $limit);
|
||||
}
|
||||
if ($source === 'spotify') {
|
||||
return array_slice($spotifyPayloads, 0, $limit);
|
||||
}
|
||||
return array_slice(array_merge($userPayloads, $spotifyPayloads), 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Album> $albums
|
||||
* @return list<string>
|
||||
*/
|
||||
private function collectSpotifyIds(array $albums): array
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($albums as $album) {
|
||||
$sid = (string) $album->getSpotifyId();
|
||||
if ($sid !== '') {
|
||||
$ids[] = $sid;
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,mixed> $searchItems
|
||||
* @return list<string>
|
||||
*/
|
||||
private function extractSpotifyIds(array $searchItems): array
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($searchItems as $item) {
|
||||
$id = isset($item['id']) ? (string) $item['id'] : '';
|
||||
if ($id !== '') {
|
||||
$ids[] = $id;
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
private function mergeStats(array $current, array $updates): array
|
||||
{
|
||||
foreach ($updates as $key => $value) {
|
||||
$current[$key] = $value;
|
||||
}
|
||||
return $current;
|
||||
}
|
||||
|
||||
private function mergeSavedIds(array $current, array $updates): array
|
||||
{
|
||||
$merged = array_merge($current, array_filter($updates, static fn($id) => $id !== ''));
|
||||
return array_values(array_unique($merged));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<mixed>> $primary
|
||||
* @param array<int,array<mixed>> $secondary
|
||||
* @return array<int,array<mixed>>
|
||||
*/
|
||||
private function mergePayloadLists(array $primary, array $secondary, int $limit): array
|
||||
{
|
||||
$seen = [];
|
||||
$merged = [];
|
||||
foreach ($primary as $payload) {
|
||||
$merged[] = $payload;
|
||||
if (isset($payload['id'])) {
|
||||
$seen[$payload['id']] = true;
|
||||
}
|
||||
if (count($merged) >= $limit) {
|
||||
return array_slice($merged, 0, $limit);
|
||||
}
|
||||
}
|
||||
foreach ($secondary as $payload) {
|
||||
$id = $payload['id'] ?? null;
|
||||
if ($id !== null && isset($seen[$id])) {
|
||||
continue;
|
||||
}
|
||||
$merged[] = $payload;
|
||||
if ($id !== null) {
|
||||
$seen[$id] = true;
|
||||
}
|
||||
if (count($merged) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return array_slice($merged, 0, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user