feat: search results

This commit is contained in:
2025-04-20 23:47:12 -05:00
parent a4ad43cfe0
commit f5552e3ad7
20 changed files with 796 additions and 113 deletions

View File

@@ -2,7 +2,8 @@
namespace App\Enum;
enum MediaType
enum MediaType: string
{
case Movie = 'movies';
case TvShow = 'tvshows';
}

View File

@@ -2,7 +2,15 @@
namespace App\Search\Action\Command;
class SearchCommand
{
use OneToMany\RichBundle\Contract\CommandInterface;
}
class SearchCommand implements CommandInterface
{
/**
* @param string $term
* @implements CommandInterface<SearchCommand>
*/
public function __construct(
public string $term
) {}
}

View File

@@ -2,7 +2,24 @@
namespace App\Search\Action\Handler;
class SearchHandler
{
use App\Search\Action\Result\SearchResult;
use App\Tmdb\Tmdb;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
}
class SearchHandler implements HandlerInterface
{
public function __construct(
private Tmdb $tmdb,
) {}
/*** @implements HandlerInterface<SearchResult> */
public function handle(CommandInterface $command): ResultInterface
{
return new SearchResult(
term: $command->term,
results: $this->tmdb->search($command->term)
);
}
}

View File

@@ -2,7 +2,25 @@
namespace App\Search\Action\Input;
class SearchInput
{
use App\Search\Action\Command\SearchCommand;
use OneToMany\RichBundle\Attribute\SourceQuery;
use OneToMany\RichBundle\Attribute\SourceRequest;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
}
/**
* @implements InputInterface<SearchCommand>
*/
class SearchInput implements InputInterface
{
public function __construct(
#[SourceQuery('term')]
#[SourceRequest('term')]
public string $term
){}
public function toCommand(): CommandInterface
{
return new SearchCommand($this->term);
}
}

View File

@@ -2,7 +2,12 @@
namespace App\Search\Action\Result;
class SearchResult
{
use OneToMany\RichBundle\Contract\ResultInterface;
}
class SearchResult implements ResultInterface
{
public function __construct(
public string $term = "",
public array $results = []
) {}
}

View File

@@ -2,17 +2,27 @@
namespace App\Search\Framework\Controller;
use App\Search\Action\Handler\SearchHandler;
use App\Search\Action\Input\SearchInput;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class SearchController extends AbstractController
{
public function __construct(
private SearchHandler $searchHandler,
) {}
#[Route('/search', name: 'app_search', methods: ['GET'])]
public function index(): Response
public function index(
SearchInput $searchInput,
): Response
{
$results = $this->searchHandler->handle($searchInput->toCommand());
return $this->render('search/results.html.twig', [
'controller_name' => 'SearchController',
'results' => $results,
]);
}
}

View File

@@ -2,7 +2,8 @@
namespace App\Tmdb;
use App\ValueObject\MediaResult;
use App\Enum\MediaType;
use App\Tmdb\TmdbResult;
use App\ValueObject\ResultFactory;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -31,7 +32,7 @@ use Tmdb\Repository\TvSeasonRepository;
use Tmdb\Token\Api\ApiToken;
use Tmdb\Token\Api\BearerToken;
class Client
class Tmdb
{
protected Client $client;
@@ -178,8 +179,8 @@ class Client
throw new \Exception("A media type must be set when parsing from an array.");
}
function parseTvShow(array $data, string $posterBasePath): MediaResult {
return new MediaResult(
function parseTvShow(array $data, string $posterBasePath): TmdbResult {
return new TmdbResult(
imdbId: $data['external_ids']['imdb_id'],
tvdbId: $data['id'],
title: $data['name'],
@@ -191,8 +192,8 @@ class Client
);
}
function parseMovie(array $data, string $posterBasePath): MediaResult {
return new MediaResult(
function parseMovie(array $data, string $posterBasePath): TmdbResult {
return new TmdbResult(
imdbId: $data['external_ids']['imdb_id'],
tvdbId: $data['id'],
title: $data['title'],
@@ -209,67 +210,80 @@ class Client
return $result;
}
private function parseFromObject($result)
private function parseFromObject($result): TmdbResult
{
$result->mediaType = $result instanceof Movie ? 'movies' : 'tvshows';
$mediaType = $result instanceof Movie ? MediaType::Movie->value : MediaType::TvShow->value;
$tmdbResult = new TmdbResult();
$tmdbResult->mediaType = $mediaType;
$tmdbResult->imdbId = $this->getImdbId($result->getId(), $mediaType);
$tmdbResult->title = $this->getTitle($result, $mediaType);
$tmdbResult->poster = self::POSTER_IMG_PATH . $result->getPosterImage();
$tmdbResult->year = $this->getReleaseDate($result, $mediaType);
$tmdbResult->description = $result->getOverview();
return $tmdbResult;
}
$externalIds = $this->cache->get("tmdb.externalIds.{$result->getId()}", function (ItemInterface $item) use ($result) {
if ($result instanceof Movie) {
$externalIds = $this->movieRepository->getExternalIds($result->getId());
} else {
$externalIds = $this->tvRepository->getExternalIds($result->getId());
}
return $externalIds;
public function getImdbId(string $tmdbId, $mediaType)
{
$externalIds = $this->cache->get("tmdb.externalIds.{$tmdbId}",
function (ItemInterface $item) use ($tmdbId, $mediaType) {
switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->value:
return $this->movieRepository->getExternalIds($tmdbId);
case MediaType::TvShow->value:
return $this->tvRepository->getExternalIds($tmdbId);
default:
return null;
}
});
$images = $this->cache->get("tmdb.images.{$result->getId()}", function (ItemInterface $item) use ($result) {
if ($result instanceof Movie) {
$images = $this->movieRepository->getImages($result->getId());
} else {
$images = $this->tvRepository->getImages($result->getId());
}
return $images;
if (null === $externalIds) {
return null;
}
return $externalIds->getImdbId() !== "" ? $externalIds->getImdbId() : "null";
}
public function getImages($tmdbId, $mediaType)
{
return $this->cache->get("tmdb.images.{$tmdbId}",
function (ItemInterface $item) use ($tmdbId, $mediaType) {
switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->value:
return $this->movieRepository->getImages($tmdbId);
case MediaType::TvShow->value:
return $this->tvRepository->getImages($tmdbId);
default:
return null;
}
});
}
if (null !== $externalIds) {
$imdbId = $externalIds->getImdbId() !== "" ? $externalIds->getImdbId() : "null";
if ("movies" === $result->mediaType) {
$result->setImdbId($imdbId);
} else {
$result->imdbId = $imdbId;
$result->title = $result->getName();
}
} else {
if ("movies" === $result->mediaType) {
$result->setImdbId("null");
} else {
$result->imdbId = "null";
}
private function getReleaseDate($result, $mediaType): string
{
switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->value:
return ($result->getReleaseDate() instanceof \DateTime)
? $result->getReleaseDate()->format('Y')
: $result->getReleaseDate();
case MediaType::TvShow->value:
return ($result->getFirstAirDate() instanceof \DateTime)
? $result->getFirstAirDate()->format('Y')
: $result->getFirstAirDate();
default:
return "";
}
}
if ("movies" === $result->mediaType) {
if ($result->getReleaseDate() instanceof \DateTime) {
$result->year = $result->getReleaseDate()->format("Y");
} else {
$result->year = (new \DateTime($result->getReleaseDate()))->format('Y');
}
} else {
if ($result->getFirstAirDate() instanceof \DateTime) {
$result->year = $result->getFirstAirDate()->format("Y");
} else {
$result->year = (new \DateTime($result->getFirstAirDate()))->format('Y');
}
private function getTitle($result, $mediaType): string
{
switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->value:
return $result->getTitle();
case MediaType::TvShow->value:
return $result->getName();
default:
return "";
}
/** @var Movie $result */
$result->setExternalIds($externalIds);
$result->setImages($images);
$result->getPosterImage()->setFilePath(
self::POSTER_IMG_PATH . $result->getPosterImage()->getFilePath()
);
return $result;
}
}

View File

@@ -1,17 +1,17 @@
<?php
namespace App\ValueObject;
namespace App\Tmdb;
class MediaResult
class TmdbResult
{
public function __construct(
public string $imdbId,
public string $tvdbId,
public string $title,
public string $poster,
public string $excerpt,
public string $year,
public string $mediaType,
public ?string $imdbId = "",
public ?string $tvdbId = "",
public ?string $title = "",
public ?string $poster = "",
public ?string $description = "",
public ?string $year = "",
public ?string $mediaType = "",
public ?array $episodes = null,
) {}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class SearchResult
{
}