Files
mtdb_movie/app/Services/Data/Tmdb/TransformData.php
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

523 lines
16 KiB
PHP
Executable File

<?php
namespace App\Services\Data\Tmdb;
use App\Actions\Titles\HandlesEncodedTmdbId;
use App\Models\Episode;
use App\Models\Person;
use App\Models\Season;
use App\Models\Title;
use App\Models\Video;
use Carbon\Carbon;
use Common\Settings\Settings;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class TransformData
{
use HandlesEncodedTmdbId;
public const TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p/original';
public const BACKDROP_BASE_URI = 'https://image.tmdb.org/t/p/w1280';
public const PROFILE_BASE_URI = 'https://image.tmdb.org/t/p/w185';
public const YOUTUBE_BASE_URI = 'https://youtube.com/embed/';
public const SERIES_ENDED_STATUS = ['Ended', 'Canceled'];
public function __construct(private Settings $settings)
{
}
public function execute(array $tmdbMedia): Collection
{
return collect($tmdbMedia)->map(
fn($mediaItem) => $this->transformMediaItem($mediaItem),
);
}
public function transformMediaItem($mediaItem): ?array
{
$type = $this->getType($mediaItem);
if ($type === Person::MODEL_TYPE) {
return $this->transformPerson($mediaItem);
} elseif ($type === Episode::MODEL_TYPE) {
return $this->transformEpisode($mediaItem);
} elseif ($type === Season::MODEL_TYPE) {
return $this->transformSeason($mediaItem);
} else {
return $this->transformTitle($mediaItem, $type);
}
}
private function transformTitle($data, $type): array
{
$releaseKey =
$type === Title::MOVIE_TYPE ? 'release_date' : 'first_air_date';
$releaseDate = $this->getReleaseDate($releaseKey, $data);
$name = $this->getTitle($data);
$transformed = [
'id' => $this->encodeTmdbId('tmdb', $type, $data['id']),
'is_series' => $type === Title::SERIES_TYPE,
'model_type' => Title::MODEL_TYPE,
'poster' => $this->getPoster(Arr::get($data, 'poster_path')),
'release_date' => $releaseDate,
'cast' => $this->getCredits($data),
'name' => $name,
'description' => $data['overview'],
'tmdb_vote_count' => $data['vote_count'] ?? null,
'tmdb_vote_average' => isset($data['vote_average'])
? round($data['vote_average'], 1)
: null,
'original_title' => $this->getOriginalName($data),
'popularity' => Arr::get($data, 'popularity'),
'language' => Arr::get($data, 'original_language'),
'certification' => $this->getCertification($data, $type),
'countries' => $this->getCountries($data),
'tagline' => Arr::get($data, 'tagline'),
'budget' => Arr::get($data, 'budget') ?: null,
'revenue' => Arr::get($data, 'revenue') ?: null,
'runtime' => $this->getRuntime($data),
'videos' => $this->formatVideos(
Arr::get($data, 'videos.results', []),
),
'images' => $this->transformImages($data),
'backdrop' => $this->getBackdrop($data),
'genres' => $this->getGenres($data),
'imdb_id' => Arr::get($data, 'external_ids.imdb_id') ?: null,
'tmdb_id' => $data['id'],
'keywords' => $this->getKeywords($data),
'series_ended' => (bool) in_array(
Arr::get($data, 'status'),
self::SERIES_ENDED_STATUS,
),
'adult' => Arr::get($data, 'adult', false),
];
if (Arr::get($data, 'seasons')) {
$transformed['seasons'] = $this->getSeasons($data);
}
return $transformed;
}
/**
* Get US certification for title.
*
* @param array $data
* @param $type
* @return string|null
*/
private function getCertification($data, $type)
{
if ($type === Title::SERIES_TYPE) {
$firstKey = 'content_ratings.results';
$secondKey = 'rating';
} else {
$firstKey = 'release_dates.results';
$secondKey = 'release_dates.*.certification';
}
$rating = collect(Arr::get($data, $firstKey, []))
->where('iso_3166_1', 'US')
->pluck($secondKey)
->flatten()
->filter()
->first();
return $rating ? str_replace('tv-', '', strtolower($rating)) : null;
}
private function getCountries($data)
{
return array_map(
fn($country) => [
'name' => strtolower($country['iso_3166_1']),
'display_name' => $country['name'],
],
Arr::get($data, 'production_countries', []),
);
}
private function transformEpisode(array $data): array
{
$releaseDate = $this->getReleaseDate('air_date', $data);
$epNum = Arr::get($data, 'episode_number');
$sNum = Arr::get($data, 'season_number');
return [
'id' => $this->encodeTmdbId(
'tmdb',
Episode::MODEL_TYPE,
$data['id'],
),
'model_type' => Episode::MODEL_TYPE,
'poster' => $this->getPoster(Arr::get($data, 'still_path')),
'release_date' => $releaseDate,
'cast' => $this->getCredits($data),
// episode might not have a name sometimes, auto generate it in that case
'name' => $this->getTitle($data) ?? "Episode #$sNum.$epNum",
'description' => $data['overview'],
'tmdb_vote_count' => $data['vote_count'],
'tmdb_vote_average' => round($data['vote_average'], 1) ?: null,
'popularity' => Arr::get($data, 'popularity'),
'episode_number' => $epNum,
'season_number' => $sNum,
'runtime' => $data['runtime'] ?? null,
];
}
private function getPoster(?string $path): ?string
{
return $path ? self::TMDB_IMAGE_BASE . $path : null;
}
private function getBackdrop($data): ?string
{
$backdrop = Arr::get($data, 'backdrop_path');
return $backdrop ? self::BACKDROP_BASE_URI . $backdrop : null;
}
private function getSeasons(array $data): ?array
{
if (!Arr::has($data, 'seasons')) {
return null;
}
// skip "specials" season with number of "0"
$seasons = array_filter(
Arr::get($data, 'seasons', []),
fn($season) => $season['season_number'] !== 0,
);
return array_map(
fn($season) => Arr::except(
$this->transformSeason($season),
'model_type',
),
$seasons,
);
}
private function transformSeason(array $data): array
{
$releaseDate = $this->getReleaseDate(
['first_air_date', 'air_date'],
$data,
);
$transformedData = [
'id' => $this->encodeTmdbId(
'tmdb',
Season::MODEL_TYPE,
$data['id'],
),
'model_type' => Season::MODEL_TYPE,
'poster' => $this->getPoster(Arr::get($data, 'poster_path')),
'release_date' => $releaseDate,
'number' => Arr::get($data, 'season_number'),
];
if (isset($data['credits'])) {
$transformedData['cast'] = $this->getCredits($data);
}
if (isset($data['episodes'])) {
$transformedData['episodes'] = array_map(
fn($episode) => $this->transformEpisode($episode),
$data['episodes'],
);
}
return $transformedData;
}
private function getKeywords(array $data): array
{
$keywords = array_merge(
Arr::get($data, 'keywords.results', []),
Arr::get($data, 'keywords.keywords', []),
);
return array_map(
fn($keyword) => ['name' => $keyword['name']],
$keywords,
);
}
private function getRuntime(array $data): ?int
{
$runtime = Arr::get(
$data,
'runtime',
Arr::get($data, 'episode_run_time'),
);
if (is_array($runtime)) {
$runtime = !empty($runtime) ? min($runtime) : null;
}
return $runtime;
}
private function transformPerson($tmdbPerson): ?array
{
if (!isset($tmdbPerson['id'])) {
return null;
}
$syncCredits = $this->settings->get('content.automate_filmography');
$hasCredits = Arr::has($tmdbPerson, 'combined_credits') && $syncCredits;
$hasKnownForCredits =
Arr::has($tmdbPerson, 'known_for') && $syncCredits;
$data = [
'id' => $this->encodeTmdbId(
'tmdb',
Person::MODEL_TYPE,
$tmdbPerson['id'],
),
'name' => $tmdbPerson['name'],
'tmdb_id' => $tmdbPerson['id'],
'imdb_id' => Arr::get($tmdbPerson, 'imdb_id'),
'gender' => $this->transformGender(Arr::get($tmdbPerson, 'gender')),
'poster' => $this->getPoster($tmdbPerson['profile_path']),
'model_type' => Person::MODEL_TYPE,
'adult' => Arr::get($tmdbPerson, 'adult', false),
'fully_synced' => Arr::get($tmdbPerson, 'fully_synced') ?: false,
'relation_data' => [
'character' => Arr::get($tmdbPerson, 'character') ?: null,
'order' => Arr::get($tmdbPerson, 'order', 0),
'department' => strtolower(
Arr::get($tmdbPerson, 'department', 'actors'),
),
'job' => strtolower(Arr::get($tmdbPerson, 'job', 'actor')),
],
];
// "known_for" credits will only be returned from "search" tmdb api call.
if (
!$hasCredits &&
$hasKnownForCredits &&
isset($tmdbPerson['known_for'][0])
) {
$data['primary_credit'] = $this->transformMediaItem(
$tmdbPerson['known_for'][0],
);
}
if ($hasCredits) {
$credits = array_merge(
Arr::get($tmdbPerson, 'combined_credits.cast'),
Arr::get($tmdbPerson, 'combined_credits.crew'),
);
$credits = array_map(function ($credit) {
$title = array_filter(
$this->transformMediaItem($credit),
fn($value) => !is_array($value),
);
$title['relation_data'] = [
'department' => strtolower(
Arr::get($credit, 'department', 'actors'),
),
'job' => strtolower(Arr::get($credit, 'job', 'actor')),
'character' => Arr::get($credit, 'character') ?: null,
'order' => Arr::get($credit, 'order', 0),
];
return $title;
}, $credits);
$credits = array_filter(
$credits,
fn($credit) => !Arr::get($credit, 'adult') ||
$this->settings->get('tmdb.includeAdult'),
);
$data['credits'] = $credits;
}
$optionalProps = [
'biography' => 'description',
'birthday' => 'birth_date',
'deathday' => 'death_date',
'place_of_birth' => 'birth_place',
'known_for_department' => 'known_for',
'popularity' => 'popularity',
];
// can't set these as "null" as some data might not be contained
// when getting people via movie/series filmography
foreach ($optionalProps as $tmdbKey => $localKey) {
if (Arr::has($tmdbPerson, $tmdbKey)) {
$data[$localKey] = $tmdbPerson[$tmdbKey];
}
}
return $data;
}
private function getCredits(array $tmdbTitle): array
{
// cast/crew from series, movies and episodes
$credits = array_merge(
Arr::get($tmdbTitle, 'credits.cast', []),
Arr::get($tmdbTitle, 'credits.crew', []),
Arr::get($tmdbTitle, 'crew', []),
Arr::get($tmdbTitle, 'guest_stars', []),
);
if ($createdBy = Arr::get($tmdbTitle, 'created_by')) {
$creators = array_map(function ($person) {
$person['job'] = 'creator';
$person['department'] = 'creators';
$person['known_for_department'] = 'creators';
$person['popularity'] = 3;
return $person;
}, $createdBy);
$credits = array_merge($credits, $creators);
}
$transformedCredits = array_map(
fn($person) => $this->transformPerson($person),
$credits,
);
return array_filter($transformedCredits);
}
/**
* @param int|null $gender
* @return null|string
*/
private function transformGender($gender)
{
if ($gender === 1) {
return 'female';
} elseif ($gender === 2) {
return 'male';
} else {
return null;
}
}
private function transformImages(array $tmdbTitle): array
{
$images = Arr::get($tmdbTitle, 'images.backdrops', []);
return array_map(
fn($image) => [
'type' => 'backdrop',
'source' => 'tmdb',
'url' => self::TMDB_IMAGE_BASE . $image['file_path'],
],
$images,
);
}
/**
* @param $tmdbTitle
* @return array
*/
private function getGenres($tmdbTitle)
{
return array_map(
fn($genre) => ['name' => $genre['name']],
Arr::get($tmdbTitle, 'genres', []),
);
}
public function formatVideos(array $videos): array
{
$videos = array_map(
fn($video) => [
'name' => trim($video['name']),
'src' => self::YOUTUBE_BASE_URI . $video['key'],
'type' => Video::VIDEO_TYPE_EMBED,
'origin' => 'tmdb',
'category' => strtolower(Arr::get($video, 'type', 'trailer')),
],
$videos,
);
// show trailers first
usort($videos, function ($a, $b) {
if ($a['category'] === 'trailer') {
return -1;
} elseif ($b['category'] === 'trailer') {
return 1;
} else {
return 0;
}
});
return $videos;
}
/**
* @param array $data
* @return string
*/
private function getType($data)
{
$hasSeasonNumber = Arr::get($data, 'season_number');
$hasEpisodeNumber = Arr::get($data, 'episode_number');
if ($hasEpisodeNumber && $hasSeasonNumber) {
return Episode::MODEL_TYPE;
} elseif ($hasSeasonNumber) {
return Season::MODEL_TYPE;
} elseif (
Arr::get($data, 'media_type') === 'person' ||
Arr::has($data, 'gender')
) {
return Person::MODEL_TYPE;
} elseif (Arr::has($data, 'first_air_date')) {
return Title::SERIES_TYPE;
} else {
return Title::MOVIE_TYPE;
}
}
private function getReleaseDate(string|array $key, array $data): ?Carbon
{
$potentials = !is_array($key) ? [$key] : $key;
foreach ($potentials as $potential) {
if (isset($data[$potential])) {
return Carbon::parse($data[$potential]);
}
}
return null;
}
/**
* @param array $tmdbTitle
* @return string|null
*/
private function getTitle($tmdbTitle)
{
if (isset($tmdbTitle['title'])) {
return $tmdbTitle['title'];
} elseif (isset($tmdbTitle['name'])) {
return $tmdbTitle['name'];
} else {
return null;
}
}
/**
* @param array $tmdbTitle
* @return string|null
*/
private function getOriginalName($tmdbTitle)
{
if (isset($tmdbTitle['original_title'])) {
return $tmdbTitle['original_title'];
} elseif (isset($tmdbTitle['original_name'])) {
return $tmdbTitle['original_name'];
} else {
return null;
}
}
}