275 lines
11 KiB
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;
|
|
}
|
|
}
|