'integer', 'allow_update' => 'boolean', 'series_ended' => 'boolean', 'is_series' => 'boolean', 'tmdb_vote_count' => 'integer', 'runtime' => 'integer', 'views' => 'integer', 'popularity' => 'integer', 'tmdb_vote_average' => 'float', 'local_vote_average' => 'float', 'fully_synced' => 'boolean', 'adult' => 'boolean', 'rating' => 'float', 'vote_count' => 'integer', 'seasons_count' => 'integer', 'release_date' => 'date', ]; protected static function booted() { static::addGlobalScope('adult', function (Builder $builder) { if (!config('tmdb.includeAdult')) { $builder->where('adult', false); } }); } public function findSeason(int $number): Season { return $this->seasons() ->where('number', $number) ->firstOrFail(); } public function findEpisode(int $season, int $episode): Episode { return $this->episodes() ->where('season_number', $season) ->where('episode_number', $episode) ->firstOrFail(); } public function maybeUpdateFromExternal(array $options = []): static|null { $tmdbImportingIsEnabled = settings('content.title_provider') === 'tmdb' || Arr::get($options, 'forceAutomation'); $needsUpdating = $this->needsUpdating( $options['ignoreLastUpdate'] ?? false, ); // first update title itself, if needed if ($tmdbImportingIsEnabled && $needsUpdating) { $data = app(TmdbApi::class)->getTitle($this); if (!$data) { return null; } app(StoreTitleData::class)->execute($this, $data); } // then update 3 last seasons if ( $needsUpdating && $this->is_series && Arr::get($options, 'updateLast3Seasons') && (settings('content.force_season_update') || $tmdbImportingIsEnabled) ) { $this->seasons() ->orderBy('number', 'desc') ->take(3) ->get() ->each( fn(Season $season) => $season->maybeUpdateFromExternal( $this, $options, ), ); } return $this; } protected function needsUpdating($force = false): bool { if (!$this->tmdb_id || !$this->exists) { return false; } if ($force) { return true; } // only partial data was fetched if ( is_null($this->release_date) || (is_null($this->runtime) && is_null($this->revenue) && is_null($this->country) && is_null($this->budget) && is_null($this->imdb_id)) ) { return true; } // sync every week return $this->allow_update && $this->updated_at->lessThan(Carbon::now()->subWeek()); } public static function firstOrCreateFromEncodedTmdbId( string $encodedId, ): static { [$tmdbId, $type] = static::decodeTmdbIdOrFail($encodedId); if (!$tmdbId || !$type) { throw new ModelNotFoundException(); } return static::withoutGlobalScope('adult')->firstOrCreate([ 'tmdb_id' => $tmdbId, 'is_series' => $type === Title::SERIES_TYPE, ]); } protected function rating(): Attribute { return Attribute::make( get: function () { return (float) Arr::get( $this->attributes, config('common.site.rating_column'), ); }, ); } protected function status(): Attribute { return Attribute::make( get: function () { if ($this->release_date?->isFuture()) { return 'upcoming'; } elseif ($this->is_series) { return $this->series_ended ? 'ended' : 'ongoing'; } else { return 'released'; } }, ); } protected function voteCount(): Attribute { return Attribute::make( get: function () { $column = str_replace( '_average', '_count', config('common.site.rating_column'), ); return Arr::get($this->attributes, $column) ?: 0; }, ); } protected function year(): Attribute { return Attribute::make( get: function () { return $this->release_date?->year; }, ); } public function plays(): HasManyThrough { return $this->hasManyThrough(VideoPlay::class, Video::class); } public function genres(): BelongsToMany { return $this->belongsToMany(Genre::class); } public function keywords(): BelongsToMany { return $this->belongsToMany(Keyword::class); } public function productionCountries(): BelongsToMany { return $this->belongsToMany(ProductionCountry::class, 'country_title'); } public function scopeCompact(Builder $query): Builder { return $query->select([ 'titles.id', 'titles.name', 'titles.poster', 'titles.backdrop', ]); } public function images(): MorphMany { return $this->morphMany(Image::class, 'model') ->select(['id', 'model_id', 'model_type', 'url', 'type', 'source']) ->orderBy('order', 'asc'); } public function newsArticles(): MorphToMany { return $this->morphToMany( NewsArticle::class, 'model', 'news_article_models', 'model_id', 'article_id', )->orderBy('created_at', 'desc'); } public function reviews(): MorphMany { return $this->morphMany(Review::class, 'reviewable'); } public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable') ->orderBy('created_at', 'desc') ->orderByWeightedScore(); } public function seasons(): HasMany { return $this->hasMany(Season::class); } public function season(): HasOne { return $this->hasOne(Season::class); } public function episodes(): HasMany { return $this->hasMany(Episode::class); } public function toSearchableArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'original_title' => $this->original_title, 'release_date' => $this->release_date, 'popularity' => $this->popularity, 'created_at' => $this->created_at->timestamp ?? '_null', 'updated_at' => $this->updated_at->timestamp ?? '_null', ]; } public static function filterableFields(): array { return ['id', 'created_at', 'updated_at', 'release_date', 'popularity']; } public function toNormalizedArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'description' => $this->release_date?->format('Y'), 'image' => $this->poster ? preg_replace('/original|w1280/', 'w92', $this->poster) : null, 'model_type' => self::MODEL_TYPE, ]; } public static function getModelTypeAttribute(): string { return self::MODEL_TYPE; } public function resolveRouteBinding($value, $field = null): static { if (is_numeric($value) || ctype_digit($value)) { return $this->findOrFail($value); } [$tmdbId, $type] = static::decodeTmdbIdOrFail($value); if (!$tmdbId || !$type) { throw new ModelNotFoundException(); } return static::where('tmdb_id', $tmdbId) ->where('is_series', $type === Title::SERIES_TYPE) ->firstOrFail(); } }