PIMS/app/Modules/Movies/Services/Tmdb/TmdbMovieProvider.php

171 lines
7 KiB
PHP
Raw Normal View History

<?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, "Couldnt 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, "Couldnt 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;
}
}