171 lines
7 KiB
PHP
171 lines
7 KiB
PHP
<?php
|
||
|
||
namespace App\Modules\Movies\Services\Tmdb;
|
||
|
||
use App\Modules\Movies\Services\Contracts\MovieProvider;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\Http;
|
||
use Illuminate\Support\Collection;
|
||
|
||
class TmdbMovieProvider implements MovieProvider
|
||
{
|
||
protected string $baseUrl;
|
||
protected ?string $apiToken;
|
||
protected string $language;
|
||
protected string $imageBase;
|
||
protected string $posterSize;
|
||
protected string $backdropSize;
|
||
protected string $profileSize;
|
||
protected int $cacheTtl;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->baseUrl = rtrim(config('movies.tmdb.base_url', 'https://api.themoviedb.org/3'), '/');
|
||
$this->apiToken = config('movies.tmdb.api_token');
|
||
$this->language = (string) config('movies.tmdb.language', 'en-US');
|
||
$this->imageBase = rtrim(config('movies.tmdb.image_base', 'https://image.tmdb.org/t/p/'), '/');
|
||
$this->posterSize = (string) config('movies.tmdb.poster_size', 'w500');
|
||
$this->backdropSize = (string) config('movies.tmdb.backdrop_size', 'w780');
|
||
$this->profileSize = (string) config('movies.tmdb.profile_size', 'w185');
|
||
$this->cacheTtl = (int) config('movies.tmdb.cache_ttl', 3600);
|
||
}
|
||
|
||
public function search(string $query, int $page = 1): array
|
||
{
|
||
$query = trim($query);
|
||
if ($query === '') {
|
||
return ['results' => collect(), 'total' => 0, 'page' => 1, 'has_more' => false];
|
||
}
|
||
|
||
$cacheKey = sprintf('tmdb.search.%s.p%s.%s', md5($query), $page, $this->language);
|
||
$data = Cache::remember($cacheKey, $this->cacheTtl, function () use ($query, $page) {
|
||
$response = Http::acceptJson()
|
||
->withHeaders($this->authHeaders())
|
||
->timeout(6)
|
||
->retry(3, 300)
|
||
->get($this->baseUrl . '/search/movie', [
|
||
'query' => $query,
|
||
'page' => $page,
|
||
'include_adult' => false,
|
||
'language' => $this->language,
|
||
]);
|
||
|
||
if ($response->failed()) {
|
||
abort(502, "Couldn’t reach TMDb. Please try again.");
|
||
}
|
||
|
||
return $response->json();
|
||
});
|
||
|
||
$total = (int) ($data['total_results'] ?? 0);
|
||
$hasMore = ($data['page'] ?? 1) < ($data['total_pages'] ?? 1);
|
||
$results = collect($data['results'] ?? [])->map(function ($item) {
|
||
return [
|
||
'provider' => 'tmdb',
|
||
'provider_id' => isset($item['id']) ? (string) $item['id'] : null,
|
||
'title' => $item['title'] ?? ($item['original_title'] ?? null),
|
||
'year' => isset($item['release_date']) && $item['release_date'] ? (int) substr($item['release_date'], 0, 4) : null,
|
||
'poster_url' => $this->buildImageUrl($item['poster_path'] ?? null, true),
|
||
];
|
||
});
|
||
|
||
return [
|
||
'results' => $results,
|
||
'total' => $total,
|
||
'page' => (int) ($data['page'] ?? $page),
|
||
'has_more' => $hasMore,
|
||
];
|
||
}
|
||
|
||
public function details(string $id): array
|
||
{
|
||
$id = trim($id);
|
||
$cacheKey = sprintf('tmdb.details.%s.%s', $id, $this->language);
|
||
$data = Cache::remember($cacheKey, $this->cacheTtl, function () use ($id) {
|
||
$response = Http::acceptJson()
|
||
->withHeaders($this->authHeaders())
|
||
->timeout(6)
|
||
->retry(3, 300)
|
||
->get($this->baseUrl . "/movie/{$id}", [
|
||
'append_to_response' => 'credits,external_ids',
|
||
'language' => $this->language,
|
||
]);
|
||
|
||
if ($response->failed()) {
|
||
abort(502, "Couldn’t reach TMDb. Please try again.");
|
||
}
|
||
|
||
return $response->json();
|
||
});
|
||
|
||
if (!isset($data['id'])) {
|
||
abort(404, 'Movie not found in TMDb');
|
||
}
|
||
|
||
$genres = collect($data['genres'] ?? [])->pluck('name')->filter()->values()->all();
|
||
$cast = collect($data['credits']['cast'] ?? [])->sortByDesc('popularity')->take(15);
|
||
$actors = $cast->pluck('name')->filter()->values()->all();
|
||
// Map of actor name => TMDb profile image URL (FULL URL), used to store on Actor records
|
||
$actorsProfiles = $cast
|
||
->mapWithKeys(function ($c) {
|
||
$name = $c['name'] ?? null;
|
||
$path = $c['profile_path'] ?? null;
|
||
if (!$name || !$path) return [];
|
||
return [$name => $this->buildProfileUrl($path)];
|
||
})
|
||
->all();
|
||
$directors = collect($data['credits']['crew'] ?? [])->filter(fn($c) => ($c['job'] ?? null) === 'Director')->pluck('name')->unique()->values()->all();
|
||
$studios = collect($data['production_companies'] ?? [])->pluck('name')->filter()->values()->all();
|
||
$countries = collect($data['production_countries'] ?? [])->pluck('name')->filter()->values()->all();
|
||
$languages = collect($data['spoken_languages'] ?? [])->pluck('english_name')->filter()->values()->all();
|
||
|
||
// External IDs include imdb_id when available
|
||
$external = [
|
||
'tmdb' => isset($data['id']) ? (string) $data['id'] : null,
|
||
];
|
||
if (!empty($data['external_ids']['imdb_id'])) {
|
||
$external['imdb'] = $data['external_ids']['imdb_id'];
|
||
}
|
||
|
||
return [
|
||
'provider' => 'tmdb',
|
||
'provider_id' => isset($data['id']) ? (string) $data['id'] : null,
|
||
'external_ids' => $external,
|
||
'title' => $data['title'] ?? null,
|
||
'original_title' => $data['original_title'] ?? ($data['title'] ?? null),
|
||
'description' => $data['overview'] ?? null,
|
||
'poster_url' => $this->buildImageUrl($data['poster_path'] ?? null, true),
|
||
'backdrop_url' => $this->buildImageUrl($data['backdrop_path'] ?? null, false),
|
||
'genres' => $genres,
|
||
'rating' => null, // Certification requires extra call; left null for now
|
||
'release_date' => $data['release_date'] ?? null,
|
||
'year' => isset($data['release_date']) && $data['release_date'] ? (int) substr($data['release_date'], 0, 4) : null,
|
||
'runtime' => isset($data['runtime']) ? (int) $data['runtime'] : null,
|
||
'actors' => $actors,
|
||
'actors_profiles' => $actorsProfiles,
|
||
'directors' => $directors,
|
||
'studios' => $studios,
|
||
'countries' => $countries,
|
||
'languages' => $languages,
|
||
];
|
||
}
|
||
|
||
protected function authHeaders(): array
|
||
{
|
||
return $this->apiToken ? ['Authorization' => 'Bearer ' . $this->apiToken] : [];
|
||
}
|
||
|
||
protected function buildImageUrl(?string $path, bool $poster = true): ?string
|
||
{
|
||
if (!$path) return null;
|
||
$size = $poster ? $this->posterSize : $this->backdropSize;
|
||
return rtrim($this->imageBase, '/') . '/' . trim($size, '/') . $path;
|
||
}
|
||
|
||
protected function buildProfileUrl(?string $path): ?string
|
||
{
|
||
if (!$path) return null;
|
||
return rtrim($this->imageBase, '/') . '/' . trim($this->profileSize, '/') . $path;
|
||
}
|
||
}
|