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; } }