CRUD Albums + Spotify API requests into DB.
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 2m17s
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 2m17s
This commit is contained in:
@@ -3,19 +3,23 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Service\SpotifyClient;
|
||||
use App\Repository\AlbumRepository;
|
||||
use App\Entity\Review;
|
||||
use App\Form\ReviewType;
|
||||
use App\Form\AlbumType;
|
||||
use App\Repository\ReviewRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Bundle\SecurityBundle\Attribute\IsGranted;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class AlbumController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'album_search', methods: ['GET'])]
|
||||
public function search(Request $request, SpotifyClient $spotifyClient, ReviewRepository $reviewRepository): Response
|
||||
public function search(Request $request, SpotifyClient $spotifyClient, ReviewRepository $reviewRepository, AlbumRepository $albumsRepo, EntityManagerInterface $em, LoggerInterface $logger): Response
|
||||
{
|
||||
$query = trim((string) $request->query->get('q', ''));
|
||||
$albumName = trim($request->query->getString('album', ''));
|
||||
@@ -27,6 +31,7 @@ class AlbumController extends AbstractController
|
||||
$yearTo = (preg_match('/^\d{4}$/', $yearToRaw)) ? (int) $yearToRaw : 0;
|
||||
$albums = [];
|
||||
$stats = [];
|
||||
$savedIds = [];
|
||||
|
||||
// Build Spotify fielded search if advanced inputs are supplied
|
||||
$advancedUsed = ($albumName !== '' || $artist !== '' || $yearFrom > 0 || $yearTo > 0);
|
||||
@@ -50,12 +55,45 @@ class AlbumController extends AbstractController
|
||||
|
||||
if ($q !== '') {
|
||||
$result = $spotifyClient->searchAlbums($q, 20);
|
||||
$albums = $result['albums']['items'] ?? [];
|
||||
if ($albums) {
|
||||
$ids = array_values(array_map(static fn($a) => $a['id'] ?? null, $albums));
|
||||
$ids = array_filter($ids, static fn($v) => is_string($v) && $v !== '');
|
||||
$searchItems = $result['albums']['items'] ?? [];
|
||||
$logger->info('Album search results received', ['query' => $q, 'items' => is_countable($searchItems) ? count($searchItems) : 0]);
|
||||
if ($searchItems) {
|
||||
// Build ordered list of IDs from search results
|
||||
$ids = [];
|
||||
foreach ($searchItems as $it) {
|
||||
$id = isset($it['id']) ? (string) $it['id'] : '';
|
||||
if ($id !== '') { $ids[] = $id; }
|
||||
}
|
||||
$ids = array_values(array_unique($ids));
|
||||
$logger->info('Album IDs extracted from search', ['count' => count($ids)]);
|
||||
|
||||
// Fetch full album objects to have consistent fields, then upsert
|
||||
$full = $spotifyClient->getAlbums($ids);
|
||||
$albumsPayload = is_array($full) ? ($full['albums'] ?? []) : [];
|
||||
if ($albumsPayload === [] && $searchItems !== []) {
|
||||
// Fallback to search items if getAlbums failed
|
||||
$albumsPayload = $searchItems;
|
||||
$logger->warning('Spotify getAlbums returned empty; falling back to search items', ['count' => count($albumsPayload)]);
|
||||
}
|
||||
$upserted = 0;
|
||||
foreach ($albumsPayload as $sa) {
|
||||
$albumsRepo->upsertFromSpotifyAlbum((array) $sa);
|
||||
$upserted++;
|
||||
}
|
||||
$em->flush();
|
||||
$logger->info('Albums upserted to DB', ['upserted' => $upserted]);
|
||||
|
||||
if ($ids) {
|
||||
$stats = $reviewRepository->getAggregatesForAlbumIds($ids);
|
||||
$existing = $albumsRepo->findBySpotifyIdsKeyed($ids);
|
||||
$savedIds = array_keys($existing);
|
||||
// Preserve Spotify order and render from DB
|
||||
$albums = [];
|
||||
foreach ($ids as $sid) {
|
||||
if (isset($existing[$sid])) {
|
||||
$albums[] = $existing[$sid]->toTemplateArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,18 +106,56 @@ class AlbumController extends AbstractController
|
||||
'year_to' => $yearTo ?: '',
|
||||
'albums' => $albums,
|
||||
'stats' => $stats,
|
||||
'savedIds' => $savedIds,
|
||||
]);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_USER')]
|
||||
#[Route('/albums/new', name: 'album_new', methods: ['GET', 'POST'])]
|
||||
public function new(Request $request, AlbumRepository $albumsRepo, EntityManagerInterface $em): Response
|
||||
{
|
||||
$album = new \App\Entity\Album();
|
||||
$album->setSource('user');
|
||||
$form = $this->createForm(AlbumType::class, $album);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
// Map artistsCsv -> artists[]
|
||||
$artistsCsv = (string) $form->get('artistsCsv')->getData();
|
||||
$artists = array_values(array_filter(array_map(static fn($s) => trim((string) $s), explode(',', $artistsCsv)), static fn($s) => $s !== ''));
|
||||
$album->setArtists($artists);
|
||||
// Assign createdBy and generate unique localId
|
||||
$u = $this->getUser();
|
||||
if ($u instanceof \App\Entity\User) {
|
||||
$album->setCreatedBy($u);
|
||||
}
|
||||
$album->setLocalId($this->generateLocalId($albumsRepo));
|
||||
$em->persist($album);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Album created.');
|
||||
return $this->redirectToRoute('album_show', ['id' => $album->getLocalId()]);
|
||||
}
|
||||
return $this->render('album/new.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/albums/{id}', name: 'album_show', methods: ['GET', 'POST'])]
|
||||
public function show(string $id, Request $request, SpotifyClient $spotifyClient, ReviewRepository $reviews, EntityManagerInterface $em): Response
|
||||
public function show(string $id, Request $request, SpotifyClient $spotifyClient, ReviewRepository $reviews, AlbumRepository $albumsRepo, EntityManagerInterface $em): Response
|
||||
{
|
||||
$album = $spotifyClient->getAlbum($id);
|
||||
if ($album === null) {
|
||||
throw $this->createNotFoundException('Album not found');
|
||||
// Prefer DB: only fetch from Spotify if not present
|
||||
$albumEntity = str_starts_with($id, 'u_') ? $albumsRepo->findOneByLocalId($id) : $albumsRepo->findOneBySpotifyId($id);
|
||||
if (!$albumEntity) {
|
||||
$spotifyAlbum = $spotifyClient->getAlbum($id);
|
||||
if ($spotifyAlbum === null) {
|
||||
throw $this->createNotFoundException('Album not found');
|
||||
}
|
||||
$albumEntity = $albumsRepo->upsertFromSpotifyAlbum($spotifyAlbum);
|
||||
$em->flush();
|
||||
}
|
||||
$isSaved = $albumEntity !== null;
|
||||
$album = $albumEntity->toTemplateArray();
|
||||
|
||||
$existing = $reviews->findBy(['spotifyAlbumId' => $id], ['createdAt' => 'DESC']);
|
||||
$existing = $reviews->findBy(['album' => $albumEntity], ['createdAt' => 'DESC']);
|
||||
$count = count($existing);
|
||||
$avg = 0.0;
|
||||
if ($count > 0) {
|
||||
@@ -90,9 +166,7 @@ class AlbumController extends AbstractController
|
||||
|
||||
// Pre-populate required album metadata before validation so entity constraints pass
|
||||
$review = new Review();
|
||||
$review->setSpotifyAlbumId($id);
|
||||
$review->setAlbumName($album['name'] ?? '');
|
||||
$review->setAlbumArtist(implode(', ', array_map(fn($a) => $a['name'], $album['artists'] ?? [])));
|
||||
$review->setAlbum($albumEntity);
|
||||
|
||||
$form = $this->createForm(ReviewType::class, $review);
|
||||
$form->handleRequest($request);
|
||||
@@ -108,12 +182,79 @@ class AlbumController extends AbstractController
|
||||
return $this->render('album/show.html.twig', [
|
||||
'album' => $album,
|
||||
'albumId' => $id,
|
||||
'isSaved' => $isSaved,
|
||||
'reviews' => $existing,
|
||||
'avg' => $avg,
|
||||
'count' => $count,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_USER')]
|
||||
#[Route('/albums/{id}/save', name: 'album_save', methods: ['POST'])]
|
||||
public function save(string $id, Request $request, SpotifyClient $spotifyClient, AlbumRepository $albumsRepo, EntityManagerInterface $em): Response
|
||||
{
|
||||
$token = (string) $request->request->get('_token');
|
||||
if (!$this->isCsrfTokenValid('save-album-' . $id, $token)) {
|
||||
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||
}
|
||||
$existing = $albumsRepo->findOneBySpotifyId($id);
|
||||
if (!$existing) {
|
||||
$spotifyAlbum = $spotifyClient->getAlbum($id);
|
||||
if ($spotifyAlbum === null) {
|
||||
throw $this->createNotFoundException('Album not found');
|
||||
}
|
||||
$albumsRepo->upsertFromSpotifyAlbum($spotifyAlbum);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Album saved.');
|
||||
} else {
|
||||
$this->addFlash('info', 'Album already saved.');
|
||||
}
|
||||
return $this->redirectToRoute('album_show', ['id' => $id]);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_USER')]
|
||||
#[Route('/albums/{id}/delete', name: 'album_delete', methods: ['POST'])]
|
||||
public function delete(string $id, Request $request, AlbumRepository $albumsRepo, EntityManagerInterface $em): Response
|
||||
{
|
||||
$token = (string) $request->request->get('_token');
|
||||
if (!$this->isCsrfTokenValid('delete-album-' . $id, $token)) {
|
||||
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||
}
|
||||
$album = str_starts_with($id, 'u_') ? $albumsRepo->findOneByLocalId($id) : $albumsRepo->findOneBySpotifyId($id);
|
||||
if ($album) {
|
||||
// Only owner or admin can delete user albums; Spotify albums require admin
|
||||
$isAdmin = $this->isGranted('ROLE_ADMIN');
|
||||
if ($album->getSource() === 'user') {
|
||||
$current = $this->getUser();
|
||||
$isOwner = false;
|
||||
if ($current instanceof \App\Entity\User) {
|
||||
$isOwner = ($album->getCreatedBy()?->getId() === $current->getId());
|
||||
}
|
||||
if (!$isAdmin && !$isOwner) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
} else {
|
||||
if (!$isAdmin) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
}
|
||||
$em->remove($album);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Album deleted.');
|
||||
} else {
|
||||
$this->addFlash('info', 'Album not found.');
|
||||
}
|
||||
return $this->redirectToRoute('album_search');
|
||||
}
|
||||
|
||||
private function generateLocalId(AlbumRepository $albumsRepo): string
|
||||
{
|
||||
do {
|
||||
$id = 'u_' . bin2hex(random_bytes(6));
|
||||
} while ($albumsRepo->findOneByLocalId($id) !== null);
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user