Files
torsearch/src/Tmdb/TmdbClient.php

275 lines
11 KiB
PHP

<?php
namespace App\Tmdb;
use Aimeos\Map;
use App\Base\Enum\MediaType;
use App\Base\Util\ImdbMatcher;
use App\Tmdb\Dto\TmdbEpisodeDto;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Tmdb\Api\Find;
use Tmdb\Client;
use Tmdb\Event\BeforeRequestEvent;
use Tmdb\Event\Listener\Psr6CachedRequestListener;
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\RequestEvent;
use Tmdb\Repository\MovieRepository;
use Tmdb\Repository\SearchRepository;
use Tmdb\Repository\TvEpisodeRepository;
use Tmdb\Repository\TvRepository;
use Tmdb\Repository\TvSeasonRepository;
use Tmdb\Token\Api\ApiToken;
use Tmdb\Token\Api\BearerToken;
class TmdbClient
{
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
protected Client $client;
protected MovieRepository $movieRepository;
protected TvRepository $tvRepository;
protected TvSeasonRepository $tvSeasonRepository;
protected TvEpisodeRepository $tvEpisodeRepository;
protected SearchRepository $searchRepository;
protected array $mediaTypeMap = [
MediaType::Movie->value => MediaType::Movie->value,
MediaType::TvShow->value => MediaType::TvShow->value,
MediaType::TvEpisode->value => MediaType::TvEpisode->value,
'movie' => 'movies',
'tv' => 'tvshows',
];
protected $repos = [];
public function __construct(
private readonly SerializerInterface $serializer,
private readonly CacheItemPoolInterface $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 Psr6CachedRequestListener(
$this->client->getHttpClient(),
$this->eventDispatcher,
$cache,
$this->client->getHttpClient()->getPsr17StreamFactory(),
[]
);
$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);
$this->tvSeasonRepository = new TvSeasonRepository($this->client);
$this->tvEpisodeRepository = new TvEpisodeRepository($this->client);
$this->searchRepository = new SearchRepository($this->client);
$this->repos = [
MediaType::Movie->value => $this->movieRepository,
MediaType::TvShow->value => $this->tvRepository,
MediaType::TvEpisode->value => $this->tvEpisodeRepository,
];
}
public function search(string $term): TmdbResult|Map
{
if (ImdbMatcher::isMatch($term)) {
$handlers = [
'movie' => 'movieDetails',
'tvshow' => 'tvshowDetails',
];
$data = $this->findByImdbId($term);
$handler = $handlers[$data['media_type']];
return $this->$handler($term);
}
$results = $this->searchRepository->getApi()->searchMulti($term);
return $this->parseListOfResults($results);
}
public function movieDetails(string $imdbId): ?TmdbResult
{
$tmdbId = $this->findByImdbId($imdbId)['id'];
return $this->parseResult(
$this->movieRepository->getApi()->getMovie($tmdbId, ['append_to_response' => 'external_ids,credits']),
MediaType::Movie->value,
$imdbId
);
}
public function tvshowDetails(string $imdbId): ?TmdbResult
{
$tmdbId = $this->findByImdbId($imdbId)['id'];
$media = $this->tvRepository->getApi()->getTvShow($tmdbId, ['append_to_response' => 'external_ids,credits']);
$media['seasons'] = Map::from($media['seasons'])->filter(function ($data) {
return $data['season_number'] !== 0 &&
strtolower($data['name']) !== 'specials' &&
$data['episode_count'] > 0;
})->map(function ($data) use ($media) {
return $this->tvSeasonDetails($media['id'], $data['season_number'])['episodes'];
})->toArray();
return $this->parseResult(
$media,
MediaType::TvShow->value,
$imdbId
);
}
public function tvSeasonDetails(string $tmdbId, int $season): array
{
$result = $this->tvSeasonRepository->getApi()->getSeason($tmdbId, $season, ['append_to_response' => 'external_ids,credits']);
$result['episodes'] = Map::from($result['episodes'])->map(function ($data) {
$data['still_path'] = self::POSTER_IMG_PATH . $data['still_path'];
$data['poster'] = $data['still_path'];
return $data;
})->rekey(fn ($data) => $data['episode_number'])->toArray();
return $result;
}
public function tvEpisodeDetails(string $tmdbId, string $showImdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
{
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids,credits']);
return $this->parseResult(
$result,
MediaType::TvEpisode->value,
$showImdbId
);
}
public function relatedMedia(string $tmdbId, string $mediaType, int $resultCount = 6): Map
{
$results = $this->repos[$mediaType]->getApi()->getRecommendations($tmdbId, ['append_to_response' => 'external_ids']);
return $this->parseListOfResults(
$results,
$resultCount
);
}
public function popularMovies(int $resultCount = 6): Map
{
$results = $this->movieRepository->getApi()->getPopular();
$results['results'] = Map::from($results['results'])->map(function ($result) {
$result['media_type'] = MediaType::Movie->value;
return $result;
});
return $this->parseListOfResults(
$results,
$resultCount
);
}
public function popularTvShows(int $resultCount = 6): Map
{
$results = $this->tvRepository->getApi()->getPopular();
$results['results'] = Map::from($results['results'])->map(function ($result) {
$result['media_type'] = MediaType::TvShow->value;
return $result;
});
return $this->parseListOfResults(
$results,
$resultCount
);
}
private function getExternalIds(int $tmdbId, string $mediaType): ?array
{
if (!array_key_exists($mediaType, $this->repos) ||
!in_array($mediaType, [MediaType::Movie->value, MediaType::TvShow->value])) {
return [];
}
return $this->repos[$mediaType]->getApi()->getExternalIds($tmdbId);
}
private function findByImdbId(string $imdbId): array
{
$finder = new Find($this->client);
$result = $finder->findBy($imdbId, ['external_source' => 'imdb_id']);
if (count($result['movie_results']) > 0) {
return $result['movie_results'][0];
} elseif (count($result['tv_results']) > 0) {
return $result['tv_results'][0];
} elseif (count($result['tv_episode_results']) > 0) {
return $result['tv_episode_results'][0];
}
throw new \Exception("No results found for $imdbId");
}
private function parseResult(array $data, string $mediaType, string $imdbId): TmdbResult|TmdbEpisodeDto
{
if (!array_key_exists('external_ids', $data)) {
$data['external_ids'] = ['imdb_id' => $imdbId];
}
return $this->serializer->denormalize($data, TmdbResult::class, context: ['media_type' => $mediaType]);
}
private function parseListOfResults(array $data, ?int $resultCount = null): Map
{
$results = Map::from($data['results'])->filter(function ($result) {
return array_key_exists('media_type', $result) &&
in_array($result['media_type'], array_keys($this->mediaTypeMap));
})->map(function ($result) {
$result['external_ids'] = $this->getExternalIds($result['id'], $this->mediaTypeMap[$result['media_type']]);
return $result;
})->filter(function ($result) {
return array_key_exists('id', $result) &&
array_key_exists('imdb_id', $result['external_ids']) &&
$result['external_ids']['imdb_id'] !== null &&
$result['external_ids']['imdb_id'] !== "";
})->map(function ($result) {
return $this->serializer->denormalize($result, TmdbResult::class, context: ['media_type' => $this->mediaTypeMap[$result['media_type']]]);
});
if (null !== $resultCount) {
$results = $results->slice(0, $resultCount);
}
return $results;
}
}