feat: search results

This commit is contained in:
2025-04-20 23:46:57 -05:00
parent f8667af685
commit a4ad43cfe0
7 changed files with 332 additions and 0 deletions

8
src/Enum/MediaType.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
namespace App\Enum;
enum MediaType
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Search\Action\Command;
class SearchCommand
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Search\Action\Handler;
class SearchHandler
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Search\Action\Input;
class SearchInput
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Search\Action\Result;
class SearchResult
{
}

275
src/Tmdb/Tmdb.php Normal file
View File

@@ -0,0 +1,275 @@
<?php
namespace App\Tmdb;
use App\ValueObject\MediaResult;
use App\ValueObject\ResultFactory;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Tmdb\Api\Find;
use Tmdb\Client;
use Tmdb\Event\BeforeRequestEvent;
use Tmdb\Event\Listener\Request\AcceptJsonRequestListener;
use Tmdb\Event\Listener\Request\ApiTokenRequestListener;
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
use Tmdb\Event\Listener\Request\UserAgentRequestListener;
use Tmdb\Event\Listener\RequestListener;
use Tmdb\Event\RequestEvent;
use Tmdb\Model\AbstractModel;
use Tmdb\Model\Image;
use Tmdb\Model\Movie;
use Tmdb\Model\Movie\QueryParameter\AppendToResponse;
use Tmdb\Model\Search\SearchQuery\KeywordSearchQuery;
use Tmdb\Model\Search\SearchQuery\MovieSearchQuery;
use Tmdb\Model\Tv;
use Tmdb\Repository\MovieRepository;
use Tmdb\Repository\SearchRepository;
use Tmdb\Repository\TvRepository;
use Tmdb\Repository\TvSeasonRepository;
use Tmdb\Token\Api\ApiToken;
use Tmdb\Token\Api\BearerToken;
class Client
{
protected Client $client;
protected MovieRepository $movieRepository;
protected TvRepository $tvRepository;
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
public function __construct(
private readonly CacheInterface $cache,
private readonly EventDispatcherInterface $eventDispatcher,
#[Autowire(env: 'TMDB_API')] string $apiKey,
) {
$this->client = new Client(
[
/** @var ApiToken|BearerToken */
'api_token' => new BearerToken($apiKey),
'secure' => true,
'base_uri' => Client::TMDB_URI,
'event_dispatcher' => [
'adapter' => $this->eventDispatcher,
],
// We make use of PSR-17 and PSR-18 auto discovery to automatically guess these, but preferably set these explicitly.
'http' => [
'client' => null,
'request_factory' => null,
'response_factory' => null,
'stream_factory' => null,
'uri_factory' => null,
],
'hydration' => [
'event_listener_handles_hydration' => false,
'only_for_specified_models' => []
]
]
);
/**
* Required event listeners and events to be registered with the PSR-14 Event Dispatcher.
*/
$requestListener = new RequestListener($this->client->getHttpClient(), $this->eventDispatcher);
$this->eventDispatcher->addListener(RequestEvent::class, $requestListener);
$apiTokenListener = new ApiTokenRequestListener($this->client->getToken());
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $apiTokenListener);
$acceptJsonListener = new AcceptJsonRequestListener();
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $acceptJsonListener);
$jsonContentTypeListener = new ContentTypeJsonRequestListener();
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $jsonContentTypeListener);
$userAgentListener = new UserAgentRequestListener();
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $userAgentListener);
$this->movieRepository = new MovieRepository($this->client);
$this->tvRepository = new TvRepository($this->client);
}
public function popularMovies(int $page = 1)
{
$movies = $this->movieRepository->getPopular(['page' => $page]);
foreach ($movies as $movie) {
$this->parseResult($movie);
}
return $movies;
}
public function search(string $term, int $page = 1)
{
$searchRepository = new SearchRepository($this->client);
$searchResults = $searchRepository->searchMulti($term, new KeywordSearchQuery(['page' => $page]));
$results = [];
foreach ($searchResults as $result) {
if (!$result instanceof Movie && !$result instanceof Tv) {
continue;
}
$results[] = $this->parseResult($result);
}
return $results;
}
public function find(string $id)
{
$finder = new Find($this->client);
$result = $finder->findBy($id, ['external_source' => 'imdb_id']);
return $this->parseResult($result);
}
public function movieDetails(string $id)
{
$client = new MovieRepository($this->client);
$details = $client->getApi()->getMovie($id, ['append_to_response' => 'external_ids']);
return $this->parseResult($details, "movie");
}
public function tvDetails(string $id)
{
$client = new TvRepository($this->client);
$details = $client->getApi()->getTvshow($id, ['append_to_response' => 'external_ids,seasons']);
$details = $this->getEpisodesFromSeries($details);
return $this->parseResult($details, "tv");
}
public function getEpisodesFromSeries(array $series)
{
$client = new TvSeasonRepository($this->client);
foreach ($series['seasons'] as $season) {
if ($season['episode_count'] <= 0 || $season['name'] === 'Specials') {
continue;
}
$series['episodes'][$season['season_number']] = $client->getApi()->getSeason($series['id'], $season['season_number'])['episodes'];
}
return $series;
}
public function mediaDetails(string $id, string $type)
{
if ($type === "movies") {
return $this->movieDetails($id);
} else {
return $this->tvDetails($id);
}
}
private function parseResult($result, $mediaType = null)
{
if (is_array($result)) {
return $this->parseFromArray($result, $mediaType);
} else {
return $this->parseFromObject($result);
}
}
private function parseFromArray($data, $mediaType)
{
if (null === $mediaType) {
throw new \Exception("A media type must be set when parsing from an array.");
}
function parseTvShow(array $data, string $posterBasePath): MediaResult {
return new MediaResult(
imdbId: $data['external_ids']['imdb_id'],
tvdbId: $data['id'],
title: $data['name'],
poster: $posterBasePath . $data['poster_path'],
excerpt: $data['overview'],
year: (new \DateTime($data['first_air_date']))->format('Y'),
mediaType: "tvshows",
episodes: $data['episodes'],
);
}
function parseMovie(array $data, string $posterBasePath): MediaResult {
return new MediaResult(
imdbId: $data['external_ids']['imdb_id'],
tvdbId: $data['id'],
title: $data['title'],
poster: $posterBasePath . $data['poster_path'],
excerpt: $data['overview'],
year: (new \DateTime($data['release_date']))->format('Y'),
mediaType: "movies",
);
}
$posterBasePath = self::POSTER_IMG_PATH;
$result = ("movie" === $mediaType) ? parseMovie($data, $posterBasePath) : parseTvShow($data, $posterBasePath);
return $result;
}
private function parseFromObject($result)
{
$result->mediaType = $result instanceof Movie ? 'movies' : 'tvshows';
$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;
});
$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) {
$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";
}
}
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');
}
}
/** @var Movie $result */
$result->setExternalIds($externalIds);
$result->setImages($images);
$result->getPosterImage()->setFilePath(
self::POSTER_IMG_PATH . $result->getPosterImage()->getFilePath()
);
return $result;
}
}

17
src/Tmdb/TmdbResult.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace App\ValueObject;
class MediaResult
{
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 ?array $episodes = null,
) {}
}