2025-12-07 03:49:26 +00:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Modules\Movies\Services;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Modules\Movies\Models\{Movie, Genre, Actor, Director, Studio, Country, Language};
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
use App\Modules\Movies\Services\Contracts\UpsertMovieServiceInterface;
|
|
|
|
|
|
|
|
|
|
|
|
class UpsertMovieFromProvider implements UpsertMovieServiceInterface
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Persist a movie and all relations from normalized provider details.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $details Output of MovieProvider::details
|
|
|
|
|
|
* @param string $mode 'overwrite'|'duplicate'
|
|
|
|
|
|
* @return Movie
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function handle(array $details, string $mode = 'overwrite'): Movie
|
|
|
|
|
|
{
|
|
|
|
|
|
return DB::transaction(function () use ($details, $mode) {
|
|
|
|
|
|
$provider = $details['provider'] ?? null;
|
|
|
|
|
|
$providerId = $details['provider_id'] ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
$movie = null;
|
|
|
|
|
|
if ($mode === 'overwrite' && $provider && $providerId) {
|
|
|
|
|
|
$movie = Movie::query()
|
|
|
|
|
|
->where('provider', $provider)
|
|
|
|
|
|
->where('provider_id', $providerId)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!$movie) {
|
|
|
|
|
|
$movie = new Movie();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fill scalar fields
|
|
|
|
|
|
$movie->fill([
|
|
|
|
|
|
'provider' => $provider,
|
|
|
|
|
|
'provider_id' => $providerId,
|
|
|
|
|
|
'external_ids' => $details['external_ids'] ?? null,
|
|
|
|
|
|
'title' => $details['title'] ?? '',
|
|
|
|
|
|
'original_title' => $details['original_title'] ?? null,
|
|
|
|
|
|
'description' => $details['description'] ?? null,
|
|
|
|
|
|
'poster_url' => $details['poster_url'] ?? null,
|
|
|
|
|
|
'backdrop_url' => $details['backdrop_url'] ?? null,
|
|
|
|
|
|
'rating' => $details['rating'] ?? null,
|
|
|
|
|
|
'release_date' => $details['release_date'] ?? null,
|
|
|
|
|
|
'year' => $details['year'] ?? null,
|
|
|
|
|
|
'runtime' => $details['runtime'] ?? null,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$movie->save();
|
|
|
|
|
|
|
|
|
|
|
|
// Sync relations using names from details payload
|
|
|
|
|
|
$this->syncByNames($movie, Genre::class, 'genres', $details['genres'] ?? []);
|
2025-12-07 07:28:02 +00:00
|
|
|
|
// Actors: also persist profile_path when available; backfill if missing
|
|
|
|
|
|
$this->syncActorsWithProfiles(
|
|
|
|
|
|
$movie,
|
|
|
|
|
|
$details['actors'] ?? [],
|
|
|
|
|
|
$details['actors_profiles'] ?? []
|
|
|
|
|
|
);
|
2025-12-07 03:49:26 +00:00
|
|
|
|
$this->syncByNames($movie, Director::class, 'directors', $details['directors'] ?? []);
|
|
|
|
|
|
$this->syncByNames($movie, Studio::class, 'studios', $details['studios'] ?? []);
|
|
|
|
|
|
$this->syncByNames($movie, Country::class, 'countries', $details['countries'] ?? []);
|
|
|
|
|
|
$this->syncByNames($movie, Language::class, 'languages', $details['languages'] ?? []);
|
|
|
|
|
|
|
|
|
|
|
|
return $movie->refresh();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @param Movie $movie
|
|
|
|
|
|
* @param class-string $modelClass
|
|
|
|
|
|
* @param string $relation
|
|
|
|
|
|
* @param array $names Names to sync
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected function syncByNames(Movie $movie, string $modelClass, string $relation, array $names): void
|
|
|
|
|
|
{
|
|
|
|
|
|
// Create or fetch IDs by name (case-insensitive normalization)
|
|
|
|
|
|
$ids = collect($names)
|
|
|
|
|
|
->filter()
|
|
|
|
|
|
->map(function ($name) use ($modelClass) {
|
|
|
|
|
|
$original = (string) $name;
|
|
|
|
|
|
$normalized = $this->normalizeName($original);
|
|
|
|
|
|
if ($normalized === '') return null;
|
|
|
|
|
|
|
|
|
|
|
|
// Find existing row in a case-insensitive way
|
|
|
|
|
|
/** @var \Illuminate\Database\Eloquent\Model|null $existing */
|
|
|
|
|
|
$existing = $modelClass::query()
|
|
|
|
|
|
->whereRaw('lower(name) = ?', [mb_strtolower($normalized)])
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($existing) {
|
|
|
|
|
|
return $existing->getKey();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @var \Illuminate\Database\Eloquent\Model $model */
|
|
|
|
|
|
$model = new $modelClass();
|
|
|
|
|
|
$model->setAttribute('name', $normalized);
|
|
|
|
|
|
$model->save();
|
|
|
|
|
|
return $model->getKey();
|
|
|
|
|
|
})
|
|
|
|
|
|
->filter()
|
|
|
|
|
|
->values()
|
|
|
|
|
|
->all();
|
|
|
|
|
|
|
|
|
|
|
|
// Sync relation if method exists
|
|
|
|
|
|
if (method_exists($movie, $relation)) {
|
|
|
|
|
|
$movie->{$relation}()->sync($ids);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 07:28:02 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Sync actors and persist their TMDb profile image path when available.
|
|
|
|
|
|
* If an actor already exists without image data, update the record.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param Movie $movie
|
|
|
|
|
|
* @param array $names List of actor names
|
|
|
|
|
|
* @param array $profiles Map of name => profile_path (relative path from TMDb)
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected function syncActorsWithProfiles(Movie $movie, array $names, array $profiles = []): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$ids = collect($names)
|
|
|
|
|
|
->filter()
|
|
|
|
|
|
->map(function ($name) use ($profiles) {
|
|
|
|
|
|
$original = (string) $name;
|
|
|
|
|
|
$normalized = $this->normalizeName($original);
|
|
|
|
|
|
if ($normalized === '') return null;
|
|
|
|
|
|
|
|
|
|
|
|
/** @var Actor|null $existing */
|
|
|
|
|
|
$existing = Actor::query()
|
|
|
|
|
|
->whereRaw('lower(name) = ?', [mb_strtolower($normalized)])
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
$profilePath = $profiles[$original] ?? $profiles[$normalized] ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
if ($existing) {
|
|
|
|
|
|
if ($profilePath && empty($existing->profile_path)) {
|
|
|
|
|
|
$existing->profile_path = $profilePath;
|
|
|
|
|
|
$existing->save();
|
|
|
|
|
|
}
|
|
|
|
|
|
return $existing->getKey();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$actor = new Actor();
|
|
|
|
|
|
$actor->setAttribute('name', $normalized);
|
|
|
|
|
|
if ($profilePath) {
|
|
|
|
|
|
$actor->setAttribute('profile_path', $profilePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
$actor->save();
|
|
|
|
|
|
return $actor->getKey();
|
|
|
|
|
|
})
|
|
|
|
|
|
->filter()
|
|
|
|
|
|
->values()
|
|
|
|
|
|
->all();
|
|
|
|
|
|
|
|
|
|
|
|
if (method_exists($movie, 'actors')) {
|
|
|
|
|
|
$movie->actors()->sync($ids);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 03:49:26 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Normalize a person/genre/studio/etc name for de-duplication.
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected function normalizeName(string $name): string
|
|
|
|
|
|
{
|
|
|
|
|
|
// Trim, collapse internal whitespace to single spaces
|
|
|
|
|
|
$name = trim(preg_replace('/\s+/u', ' ', $name) ?? '');
|
|
|
|
|
|
// Leave original casing (provider’s) but comparisons will be lowercase.
|
|
|
|
|
|
return $name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|