PIMS/app/Modules/Movies/Services/UpsertMovieFromProvider.php

173 lines
6.4 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;
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'] ?? []);
// Actors: also persist profile_path when available; backfill if missing
$this->syncActorsWithProfiles(
$movie,
$details['actors'] ?? [],
$details['actors_profiles'] ?? []
);
$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);
}
}
/**
* 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);
}
}
/**
* 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 (providers) but comparisons will be lowercase.
return $name;
}
}