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

171 lines
7 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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