193
app/Models/Channel.php
Executable file
193
app/Models/Channel.php
Executable file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Channels\FetchContentFromLocalDatabase;
|
||||
use App\Actions\Channels\FetchContentFromTmdb;
|
||||
use App\Actions\People\PaginatePeople;
|
||||
use App\Actions\Titles\Retrieve\PaginateTitles;
|
||||
use Common\Channels\BaseChannel;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Channel extends BaseChannel
|
||||
{
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'public' => 'boolean',
|
||||
'internal' => 'boolean',
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
|
||||
protected $hidden = ['pivot', 'internal'];
|
||||
|
||||
public function allTitles(array $params, $builder = null): AbstractPaginator
|
||||
{
|
||||
if (!$builder && $this->restriction) {
|
||||
$builder = $this->restriction->titles();
|
||||
}
|
||||
return (new PaginateTitles())->execute($params, $builder);
|
||||
}
|
||||
|
||||
public function allMovies(array $params, $builder = null)
|
||||
{
|
||||
if (!$builder && $this->restriction) {
|
||||
$builder = $this->restriction->titles();
|
||||
}
|
||||
$params['type'] = Title::MOVIE_TYPE;
|
||||
return (new PaginateTitles())->execute($params, $builder);
|
||||
}
|
||||
|
||||
public function allSeries(array $params, $builder = null)
|
||||
{
|
||||
if (!$builder && $this->restriction) {
|
||||
$builder = $this->restriction->titles();
|
||||
}
|
||||
$params['type'] = Title::SERIES_TYPE;
|
||||
return (new PaginateTitles())->execute($params, $builder);
|
||||
}
|
||||
|
||||
public function titles(): MorphToMany
|
||||
{
|
||||
return $this->channelableRelation(Title::class);
|
||||
}
|
||||
|
||||
public function movies(): MorphToMany
|
||||
{
|
||||
return $this->channelableRelation(Title::class)->where(
|
||||
'titles.is_series',
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
public function series(): MorphToMany
|
||||
{
|
||||
return $this->channelableRelation(Title::class)->where(
|
||||
'titles.is_series',
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
public function people(): MorphToMany
|
||||
{
|
||||
return $this->channelableRelation(Person::class);
|
||||
}
|
||||
|
||||
public function allPeople(
|
||||
array $params,
|
||||
mixed $builder = null,
|
||||
Channel $parentChannel = null,
|
||||
): AbstractPaginator {
|
||||
$params['compact'] = !is_null($parentChannel);
|
||||
return (new PaginatePeople())->execute($params, $builder);
|
||||
}
|
||||
|
||||
public function newsArticles(): MorphToMany
|
||||
{
|
||||
return $this->channelableRelation(NewsArticle::class)->select([
|
||||
'news_articles.id',
|
||||
'news_articles.title',
|
||||
'news_articles.created_at',
|
||||
]);
|
||||
}
|
||||
|
||||
public function allNewsArticles(
|
||||
array $params,
|
||||
mixed $builder = null,
|
||||
): AbstractPaginator {
|
||||
$datasource = new Datasource($builder ?? NewsArticle::query(), $params);
|
||||
|
||||
$paginator = $datasource->paginate();
|
||||
|
||||
$paginator->transform(function (NewsArticle $article) {
|
||||
$article->body = Str::limit(strip_tags($article->body), 400);
|
||||
return $article;
|
||||
});
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
protected function loadContentFromExternal(
|
||||
string $autoUpdateMethod,
|
||||
): Collection|array|null {
|
||||
$provider = Arr::get($this->config, 'autoUpdateProvider', 'local');
|
||||
$modelType = Arr::get($this->config, 'contentModel', 'movie');
|
||||
|
||||
$filters = [];
|
||||
if (isset($this->config['restriction'])) {
|
||||
$filters[$this->config['restriction']] =
|
||||
$this->config['restrictionModelId'];
|
||||
}
|
||||
|
||||
if ($provider === 'tmdb') {
|
||||
$keywords = collect();
|
||||
if (isset($filters['keyword'])) {
|
||||
$keywords->push($filters['keyword']);
|
||||
}
|
||||
if (isset($this->config['tmdb_keywords'])) {
|
||||
$keywords = $keywords
|
||||
->merge($this->config['tmdb_keywords'])
|
||||
->unique();
|
||||
}
|
||||
if (isset($this->config['tmdb_language'])) {
|
||||
$filters['language'] = $this->config['tmdb_language'];
|
||||
}
|
||||
$filters['keyword'] = $keywords->implode(',');
|
||||
|
||||
return app(FetchContentFromTmdb::class)->execute(
|
||||
$autoUpdateMethod,
|
||||
$modelType,
|
||||
$filters,
|
||||
);
|
||||
} else {
|
||||
return app(FetchContentFromLocalDatabase::class)->execute(
|
||||
$autoUpdateMethod,
|
||||
$modelType,
|
||||
$filters,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function channelableRelation(string $type): MorphToMany
|
||||
{
|
||||
return $this->morphedByMany(
|
||||
$type,
|
||||
'channelable',
|
||||
null,
|
||||
'channel_id',
|
||||
)->withPivot(['id', 'channelable_id', 'order']);
|
||||
}
|
||||
|
||||
public function resolveRouteBinding($value, $field = null)
|
||||
{
|
||||
$type = request('channelType');
|
||||
if ($value === 'watchlist') {
|
||||
if (!Auth::check()) {
|
||||
abort(401);
|
||||
}
|
||||
$channel = Auth::user()
|
||||
->watchlist()
|
||||
->firstOrFail();
|
||||
} elseif (ctype_digit($value)) {
|
||||
$channel = app(Channel::class)
|
||||
->when($type, fn($q) => $q->where('type', $type))
|
||||
->findOrFail($value);
|
||||
} else {
|
||||
$channel = app(Channel::class)
|
||||
->where('slug', $value)
|
||||
->when($type, fn($q) => $q->where('type', $type))
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
if ($channel->type === 'list') {
|
||||
$channel->load('user');
|
||||
}
|
||||
|
||||
return $channel;
|
||||
}
|
||||
}
|
||||
166
app/Models/Episode.php
Executable file
166
app/Models/Episode.php
Executable file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Titles\HasCreditableRelation;
|
||||
use App\Actions\Titles\HasVideoRelation;
|
||||
use Common\Comments\Comment;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Episode extends Model
|
||||
{
|
||||
use HasCreditableRelation, HasVideoRelation;
|
||||
|
||||
public const MODEL_TYPE = 'episode';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
protected $appends = [
|
||||
'model_type',
|
||||
'rating',
|
||||
'vote_count',
|
||||
'status',
|
||||
'year',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'episode_number' => 'integer',
|
||||
'season_number' => 'integer',
|
||||
'year' => 'integer',
|
||||
'title_id' => 'integer',
|
||||
'season_id' => 'integer',
|
||||
'allow_update' => 'boolean',
|
||||
'tmdb_vote_count' => 'integer',
|
||||
'tmdb_vote_average' => 'float',
|
||||
'popularity' => 'integer',
|
||||
'runtime' => 'integer',
|
||||
'rating' => 'float',
|
||||
'vote_count' => 'integer',
|
||||
'release_date' => 'date',
|
||||
];
|
||||
|
||||
public $hidden = [
|
||||
'imdb_rating',
|
||||
'imdb_votes_num',
|
||||
'tmdb_vote_average',
|
||||
'local_vote_average',
|
||||
'local_vote_count',
|
||||
'tmdb_vote_count',
|
||||
'mc_user_score',
|
||||
'mc_critic_score',
|
||||
];
|
||||
|
||||
protected function year(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->release_date?->year;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
protected function status(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->release_date?->isFuture()
|
||||
? 'upcoming'
|
||||
: 'released';
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
protected function rating(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return (float) Arr::get(
|
||||
$this->attributes,
|
||||
config('common.site.rating_column'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public function getRatingAttribute()
|
||||
{
|
||||
return Arr::get($this->attributes, config('common.site.rating_column'));
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public function title(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Title::class);
|
||||
}
|
||||
|
||||
public function comments(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Comment::class, 'commentable')->orderBy(
|
||||
'created_at',
|
||||
'desc',
|
||||
);
|
||||
}
|
||||
|
||||
public function plays(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(VideoPlay::class, Video::class);
|
||||
}
|
||||
|
||||
public function season(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Season::class);
|
||||
}
|
||||
|
||||
public function toNormalizedArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->relationLoaded('title')
|
||||
? $this->title->name
|
||||
: null,
|
||||
'image' => $this->poster,
|
||||
'model_type' => self::MODEL_TYPE,
|
||||
];
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'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 static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
36
app/Models/Genre.php
Executable file
36
app/Models/Genre.php
Executable file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Common\Tags\Tag;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Genre extends Tag
|
||||
{
|
||||
const MODEL_TYPE = 'genre';
|
||||
|
||||
protected $hidden = ['pivot', 'created_at', 'updated_at'];
|
||||
|
||||
public function titles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Title::class);
|
||||
}
|
||||
|
||||
public function insertOrRetrieve(
|
||||
Collection|array $tags,
|
||||
?string $type = 'custom',
|
||||
?int $userId = null,
|
||||
): Collection {
|
||||
// genre table will not have type or user_id columns
|
||||
return parent::insertOrRetrieve($tags, null, null);
|
||||
}
|
||||
|
||||
public function getByNames(
|
||||
Collection $names,
|
||||
?string $type = null,
|
||||
?int $userId = null,
|
||||
): Collection {
|
||||
return parent::getByNames($names, null, null);
|
||||
}
|
||||
}
|
||||
15
app/Models/Image.php
Executable file
15
app/Models/Image.php
Executable file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property string $source
|
||||
* @property string $url
|
||||
*/
|
||||
class Image extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
protected $hidden = ['model_id', 'model_type'];
|
||||
}
|
||||
48
app/Models/Keyword.php
Executable file
48
app/Models/Keyword.php
Executable file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Common\Tags\Tag;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Keyword extends Tag
|
||||
{
|
||||
use Searchable;
|
||||
|
||||
const MODEL_TYPE = 'keyword';
|
||||
|
||||
public function titles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Title::class);
|
||||
}
|
||||
|
||||
public function insertOrRetrieve(
|
||||
Collection|array $tags,
|
||||
?string $type = 'custom',
|
||||
?int $userId = null,
|
||||
): Collection {
|
||||
// keywords table will not have type or user_id columns
|
||||
return parent::insertOrRetrieve($tags, null, null);
|
||||
}
|
||||
|
||||
public function getByNames(
|
||||
Collection $names,
|
||||
string $type = null,
|
||||
int $userId = null,
|
||||
): Collection {
|
||||
return parent::getByNames($names, null, null);
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'display_name' => $this->display_name,
|
||||
'created_at' => $this->created_at->timestamp ?? '_null',
|
||||
'updated_at' => $this->updated_at->timestamp ?? '_null',
|
||||
];
|
||||
}
|
||||
}
|
||||
17
app/Models/Listable.php
Executable file
17
app/Models/Listable.php
Executable file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Listable extends Model
|
||||
{
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'order' => 'integer',
|
||||
'list_id' => 'integer',
|
||||
'listable_id' => 'integer',
|
||||
];
|
||||
|
||||
public const UPDATED_AT = null;
|
||||
}
|
||||
19
app/Models/Movie.php
Executable file
19
app/Models/Movie.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class Movie extends Title
|
||||
{
|
||||
protected $table = 'titles';
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::addGlobalScope('titleType', function (Builder $builder) {
|
||||
$builder->where('is_series', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
69
app/Models/NewsArticle.php
Executable file
69
app/Models/NewsArticle.php
Executable file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Common\Pages\CustomPage;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class NewsArticle extends CustomPage
|
||||
{
|
||||
const MODEL_TYPE = 'newsArticle';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $appends = ['model_type'];
|
||||
|
||||
protected function slug(): Attribute
|
||||
{
|
||||
return Attribute::make(set: fn(string $value) => slugify($value));
|
||||
}
|
||||
|
||||
public function scopeCompact(Builder $query)
|
||||
{
|
||||
return $query->select([
|
||||
'id',
|
||||
'image',
|
||||
'title',
|
||||
'slug',
|
||||
'byline',
|
||||
'source',
|
||||
'created_at',
|
||||
]);
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'body' => $this->body,
|
||||
'slug' => $this->slug,
|
||||
'source' => $this->source,
|
||||
'created_at' => $this->created_at->timestamp ?? '_null',
|
||||
'updated_at' => $this->updated_at->timestamp ?? '_null',
|
||||
];
|
||||
}
|
||||
|
||||
public function toNormalizedArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->title,
|
||||
'image' => $this->image,
|
||||
'description' => Str::limit($this->body, 100),
|
||||
'model_type' => static::MODEL_TYPE,
|
||||
];
|
||||
}
|
||||
|
||||
public static function filterableFields(): array
|
||||
{
|
||||
return ['id', 'created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return static::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
186
app/Models/Person.php
Executable file
186
app/Models/Person.php
Executable file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\People\StorePersonData;
|
||||
use App\Actions\Titles\HandlesEncodedTmdbId;
|
||||
use App\Actions\Titles\InsertsTmdbTitleOrPerson;
|
||||
use App\Services\Data\Tmdb\TmdbApi;
|
||||
use Carbon\Carbon;
|
||||
use Common\Core\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Person extends BaseModel
|
||||
{
|
||||
use Searchable, HandlesEncodedTmdbId, InsertsTmdbTitleOrPerson;
|
||||
|
||||
public const MODEL_TYPE = 'person';
|
||||
|
||||
protected $guarded = ['id', 'relation_data', 'model_type'];
|
||||
protected $appends = ['model_type'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'tmdb_id' => 'integer',
|
||||
'allow_update' => 'boolean',
|
||||
'fully_synced' => 'boolean',
|
||||
'adult' => 'boolean',
|
||||
'birth_date' => 'date',
|
||||
'death_date' => 'date',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::addGlobalScope('adult', function (Builder $builder) {
|
||||
if (!config('tmdb.includeAdult')) {
|
||||
$builder->where('adult', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeOrderByBirthDate(Builder $query, string $direction)
|
||||
{
|
||||
$query->orderByRaw(
|
||||
"CASE WHEN birth_date IS NULL THEN 1 ELSE 0 END, birth_date $direction",
|
||||
);
|
||||
}
|
||||
|
||||
public static function firstOrCreateFromEncodedTmdbId(
|
||||
string $encodedId,
|
||||
): static {
|
||||
[$tmdbId] = static::decodeTmdbIdOrFail($encodedId);
|
||||
return static::withoutGlobalScope('adult')->firstOrCreate([
|
||||
'tmdb_id' => $tmdbId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function maybeUpdateFromExternal(array $options = []): static|null
|
||||
{
|
||||
$tmdbImportingIsEnabled =
|
||||
settings('content.people_provider') === 'tmdb' ||
|
||||
Arr::get($options, 'forceAutomation');
|
||||
|
||||
if (
|
||||
$tmdbImportingIsEnabled &&
|
||||
$this->needsUpdating($options['ignoreLastUpdate'] ?? false)
|
||||
) {
|
||||
$data = app(TmdbApi::class)->getPerson($this);
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
app(StorePersonData::class)->execute($this, $data);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function needsUpdating($force = false): bool
|
||||
{
|
||||
if (!$this->exists || !$this->tmdb_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($force) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// sync every week
|
||||
return $this->allow_update &&
|
||||
(!$this->updated_at ||
|
||||
$this->updated_at->lessThan(Carbon::now()->subWeek()));
|
||||
}
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
|
||||
public function scopeCompact(Builder $query): Builder
|
||||
{
|
||||
return $query->select(['titles.id', 'titles.name', 'titles.poster']);
|
||||
}
|
||||
|
||||
public function credits(): BelongsToMany
|
||||
{
|
||||
return $this->morphedByMany(Title::class, 'creditable')
|
||||
->select(
|
||||
'titles.id',
|
||||
'is_series',
|
||||
'poster',
|
||||
'backdrop',
|
||||
'popularity',
|
||||
'name',
|
||||
'release_date',
|
||||
'tmdb_vote_average',
|
||||
'local_vote_average',
|
||||
)
|
||||
->withPivot(['id', 'job', 'department', 'order', 'character'])
|
||||
->orderBy('titles.release_date', 'desc');
|
||||
}
|
||||
|
||||
public function episodeCredits(int $tileId = null): BelongsToMany
|
||||
{
|
||||
$query = $this->morphedByMany(Episode::class, 'creditable');
|
||||
if ($tileId) {
|
||||
$query->where('episodes.title_id', $tileId);
|
||||
}
|
||||
$query
|
||||
->select(
|
||||
'episodes.id',
|
||||
'episodes.title_id',
|
||||
'name',
|
||||
'release_date',
|
||||
'season_number',
|
||||
'episode_number',
|
||||
)
|
||||
->withPivot(['job', 'department', 'order', 'character'])
|
||||
->orderBy('episodes.season_number', 'desc')
|
||||
->orderBy('episodes.episode_number', 'desc');
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $tileId
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function seasonCredits($tileId = null)
|
||||
{
|
||||
$query = $this->morphedByMany(Season::class, 'creditable');
|
||||
if ($tileId) {
|
||||
$query->where('seasons.title_id', $tileId);
|
||||
}
|
||||
$query
|
||||
->select('seasons.id', 'seasons.title_id')
|
||||
->withPivot(['job', 'department', 'order', 'character'])
|
||||
->orderBy('seasons.number', 'desc');
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'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'];
|
||||
}
|
||||
|
||||
public function toNormalizedArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'image' => $this->poster,
|
||||
'model_type' => self::MODEL_TYPE,
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Models/ProductionCountry.php
Executable file
34
app/Models/ProductionCountry.php
Executable file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Common\Tags\Tag;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ProductionCountry extends Tag
|
||||
{
|
||||
const MODEL_TYPE = 'production_country';
|
||||
|
||||
public function titles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Title::class, 'country_title');
|
||||
}
|
||||
|
||||
public function insertOrRetrieve(
|
||||
Collection|array $tags,
|
||||
?string $type = 'custom',
|
||||
?int $userId = null,
|
||||
): Collection {
|
||||
// countries table will not have type or user_id columns
|
||||
return parent::insertOrRetrieve($tags, null, null);
|
||||
}
|
||||
|
||||
public function getByNames(
|
||||
Collection $names,
|
||||
string $type = null,
|
||||
int $userId = null,
|
||||
): Collection {
|
||||
return parent::getByNames($names, null, null);
|
||||
}
|
||||
}
|
||||
19
app/Models/ProfileLink.php
Executable file
19
app/Models/ProfileLink.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProfileLink extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $visible = [
|
||||
'url', 'title'
|
||||
];
|
||||
|
||||
public function getUrlAttribute($value)
|
||||
{
|
||||
return parse_url($value, PHP_URL_SCHEME) === null ? "https://$value" : $value;
|
||||
}
|
||||
}
|
||||
98
app/Models/Review.php
Executable file
98
app/Models/Review.php
Executable file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Common\Core\BaseModel;
|
||||
use Common\Votes\OrdersByWeightedScore;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Review extends BaseModel
|
||||
{
|
||||
use OrdersByWeightedScore;
|
||||
|
||||
public const MODEL_TYPE = 'review';
|
||||
protected $guarded = ['id'];
|
||||
protected $appends = ['model_type'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'reviewable_id' => 'integer',
|
||||
'score' => 'integer',
|
||||
'has_text' => 'boolean',
|
||||
'helpful_count' => 'integer',
|
||||
'not_helpful_count' => 'integer',
|
||||
];
|
||||
|
||||
public function feedback(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReviewFeedback::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class)->select(
|
||||
'id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'avatar',
|
||||
);
|
||||
}
|
||||
|
||||
public function reviewable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function reports(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReviewReport::class);
|
||||
}
|
||||
|
||||
public function scopeOrderByMostHelpful(Builder $query): Builder
|
||||
{
|
||||
return $query->orderByWeightedScore(
|
||||
'desc',
|
||||
'helpful_count',
|
||||
'not_helpful_count',
|
||||
);
|
||||
}
|
||||
|
||||
public function scopeWithTextOnly(Builder $query): Builder
|
||||
{
|
||||
return $query->where('has_text', true);
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'body' => $this->body,
|
||||
'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'];
|
||||
}
|
||||
|
||||
public function toNormalizedArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'model_type' => self::MODEL_TYPE,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
30
app/Models/ReviewFeedback.php
Executable file
30
app/Models/ReviewFeedback.php
Executable file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReviewFeedback extends Model
|
||||
{
|
||||
protected $table = 'review_feedback';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'review_id' => 'integer',
|
||||
'is_helpful' => 'boolean',
|
||||
];
|
||||
|
||||
public function review(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Review::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
15
app/Models/ReviewReport.php
Executable file
15
app/Models/ReviewReport.php
Executable file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReviewReport extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'review_id' => 'integer',
|
||||
];
|
||||
}
|
||||
108
app/Models/Season.php
Executable file
108
app/Models/Season.php
Executable file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Titles\HasCreditableRelation;
|
||||
use App\Actions\Titles\Store\StoreSeasonData;
|
||||
use App\Services\Data\Tmdb\TmdbApi;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Season extends Model
|
||||
{
|
||||
use HasCreditableRelation;
|
||||
|
||||
public const MODEL_TYPE = 'season';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
protected $appends = ['model_type'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'fully_synced' => 'boolean',
|
||||
'episodes_count' => 'integer',
|
||||
'number' => 'integer',
|
||||
'release_date' => 'date',
|
||||
];
|
||||
|
||||
public function episodes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Episode::class);
|
||||
}
|
||||
|
||||
public function title(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Title::class);
|
||||
}
|
||||
|
||||
public function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
|
||||
public function findEpisode(int $number): Episode|null
|
||||
{
|
||||
return $this->episodes()
|
||||
->where('episode_number', $number)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
public function maybeUpdateFromExternal(
|
||||
Title $title,
|
||||
array $options = [],
|
||||
): self {
|
||||
if ($this->needsUpdating($title, $options)) {
|
||||
$data = app(TmdbApi::class)->getSeason($title, $this->number);
|
||||
if ($data) {
|
||||
app(StoreSeasonData::class)->execute($title, $data);
|
||||
$this->refresh();
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function needsUpdating(Title $title, array $options = []): bool
|
||||
{
|
||||
$isFullySynced = $options['forceAutomation'] ?? $this->fully_synced;
|
||||
$tmdbImportingIsEnabled =
|
||||
settings('content.title_provider') === 'tmdb' ||
|
||||
Arr::get($options, 'forceAutomation');
|
||||
|
||||
if (!$this->exists || !$title->tmdb_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// series ended and this season is already fully updated from external site
|
||||
if ($title->series_ended && $isFullySynced) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// season is fully synced, and it's not the latest season
|
||||
if ($isFullySynced && $title->season_count > $this->number) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!$tmdbImportingIsEnabled &&
|
||||
// might need to fetch title seasons, even if automation is disabled because they can't be
|
||||
// fetched when importing multiple titles without hitting tmdb api rate limits
|
||||
!settings('content.force_season_update')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$isFullySynced) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Arr::get($options, 'ignoreLastUpdate')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !$this->updated_at ||
|
||||
$this->updated_at->lessThan(Carbon::now()->subWeek());
|
||||
}
|
||||
}
|
||||
19
app/Models/Series.php
Executable file
19
app/Models/Series.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class Series extends Title
|
||||
{
|
||||
protected $table = 'titles';
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::addGlobalScope('titleType', function (Builder $builder) {
|
||||
$builder->where('is_series', true);
|
||||
});
|
||||
}
|
||||
}
|
||||
361
app/Models/Title.php
Executable file
361
app/Models/Title.php
Executable file
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Titles\HandlesEncodedTmdbId;
|
||||
use App\Actions\Titles\HasCreditableRelation;
|
||||
use App\Actions\Titles\HasVideoRelation;
|
||||
use App\Actions\Titles\InsertsTmdbTitleOrPerson;
|
||||
use App\Actions\Titles\Store\StoreTitleData;
|
||||
use App\Services\Data\Tmdb\TmdbApi;
|
||||
use Carbon\Carbon;
|
||||
use Common\Comments\Comment;
|
||||
use Common\Core\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Title extends BaseModel
|
||||
{
|
||||
use HasCreditableRelation,
|
||||
HasVideoRelation,
|
||||
Searchable,
|
||||
HandlesEncodedTmdbId,
|
||||
InsertsTmdbTitleOrPerson;
|
||||
|
||||
public const MOVIE_TYPE = 'movie';
|
||||
public const SERIES_TYPE = 'series';
|
||||
public const MODEL_TYPE = 'title';
|
||||
|
||||
protected $guarded = ['id', 'type'];
|
||||
protected $appends = [
|
||||
'rating',
|
||||
'model_type',
|
||||
'vote_count',
|
||||
'status',
|
||||
'year',
|
||||
];
|
||||
|
||||
public $hidden = [
|
||||
'imdb_rating',
|
||||
'imdb_votes_num',
|
||||
'tmdb_vote_average',
|
||||
'local_vote_average',
|
||||
'local_vote_count',
|
||||
'tmdb_vote_count',
|
||||
'mc_user_score',
|
||||
'mc_critic_score',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => '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();
|
||||
}
|
||||
}
|
||||
47
app/Models/User.php
Executable file
47
app/Models/User.php
Executable file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Common\Auth\BaseUser;
|
||||
use Common\Comments\Comment;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends BaseUser
|
||||
{
|
||||
use HasApiTokens;
|
||||
|
||||
public function watchlist(): HasOne
|
||||
{
|
||||
return $this->hasOne(Channel::class)
|
||||
->where('type', 'list')
|
||||
->where('name', 'watchlist');
|
||||
}
|
||||
|
||||
public function reviews(): HasMany
|
||||
{
|
||||
return $this->hasMany(Review::class);
|
||||
}
|
||||
|
||||
public function lists(): HasMany
|
||||
{
|
||||
return $this->hasMany(Channel::class)->where('type', 'list');
|
||||
}
|
||||
|
||||
public function comments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Comment::class);
|
||||
}
|
||||
|
||||
public function profile(): HasOne
|
||||
{
|
||||
return $this->hasOne(UserProfile::class);
|
||||
}
|
||||
|
||||
public function links(): MorphMany
|
||||
{
|
||||
return $this->morphMany(ProfileLink::class, 'linkeable');
|
||||
}
|
||||
}
|
||||
17
app/Models/UserProfile.php
Executable file
17
app/Models/UserProfile.php
Executable file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserProfile extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $hidden = [
|
||||
'id',
|
||||
'user_id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
}
|
||||
162
app/Models/Video.php
Executable file
162
app/Models/Video.php
Executable file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Common\Comments\Comment;
|
||||
use Common\Core\BaseModel;
|
||||
use Common\Votes\OrdersByWeightedScore;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Video extends BaseModel
|
||||
{
|
||||
use OrdersByWeightedScore;
|
||||
|
||||
public const VIDEO_TYPE_EMBED = 'embed';
|
||||
public const VIDEO_TYPE_DIRECT = 'direct';
|
||||
public const VIDEO_TYPE_EXTERNAL = 'external';
|
||||
public const MODEL_TYPE = 'video';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
protected $appends = ['score', 'model_type'];
|
||||
protected $casts = [
|
||||
'negative_votes' => 'integer',
|
||||
'positive_votes' => 'integer',
|
||||
'order' => 'integer',
|
||||
'approved' => 'boolean',
|
||||
'title_id' => 'integer',
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
|
||||
public function title(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Title::class)->select([
|
||||
'id',
|
||||
'name',
|
||||
'poster',
|
||||
'backdrop',
|
||||
'is_series',
|
||||
]);
|
||||
}
|
||||
|
||||
public function votes(): HasMany
|
||||
{
|
||||
return $this->hasMany(VideoVote::class);
|
||||
}
|
||||
|
||||
public function reports(): HasMany
|
||||
{
|
||||
return $this->hasMany(VideoReport::class);
|
||||
}
|
||||
|
||||
public function captions(): HasMany
|
||||
{
|
||||
return $this->hasMany(VideoCaption::class)->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
public function plays(): HasMany
|
||||
{
|
||||
return $this->hasMany(VideoPlay::class);
|
||||
}
|
||||
|
||||
public function latestPlay(): HasOne
|
||||
{
|
||||
return $this->hasOne(VideoPlay::class)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function episode(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Episode::class);
|
||||
}
|
||||
|
||||
public function comments(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Comment::class, 'commentable')
|
||||
->orderBy('created_at', 'desc')
|
||||
->orderByWeightedScore();
|
||||
}
|
||||
|
||||
public function scopeOrderByMostUpvotes(Builder $query): Builder
|
||||
{
|
||||
return $query->orderByWeightedScore('desc');
|
||||
}
|
||||
|
||||
public function getScoreAttribute()
|
||||
{
|
||||
$total = $this->positive_votes + $this->negative_votes;
|
||||
if (!$total) {
|
||||
return null;
|
||||
}
|
||||
return round(($this->positive_votes / $total) * 100);
|
||||
}
|
||||
|
||||
public function scopeApplySelectedSort(Builder $query): Builder
|
||||
{
|
||||
[$col, $dir] = explode(
|
||||
':',
|
||||
settings('streaming.default_sort', 'order:asc'),
|
||||
);
|
||||
|
||||
if ($col === 'score') {
|
||||
$query->orderByWeightedScore();
|
||||
} elseif ($col === 'order') {
|
||||
$query->orderByRaw('`category` = "trailer" desc, `order` asc');
|
||||
} else {
|
||||
$query
|
||||
->orderBy(DB::raw('`category` = "trailer"'), 'desc')
|
||||
->orderBy($col, $dir);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function scopeFromConfiguredCategory(Builder $builder): Builder
|
||||
{
|
||||
$contentType = settings('streaming.video_panel_content');
|
||||
|
||||
if ($contentType === 'full') {
|
||||
$builder->where('category', 'full');
|
||||
} elseif ($contentType === 'short') {
|
||||
$builder->where('category', '!=', 'full');
|
||||
} elseif ($contentType !== 'all') {
|
||||
$builder->where('category', $contentType);
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'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'];
|
||||
}
|
||||
|
||||
public function toNormalizedArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'image' => $this->thumbnail,
|
||||
'model_type' => self::MODEL_TYPE,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
22
app/Models/VideoCaption.php
Executable file
22
app/Models/VideoCaption.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class VideoCaption extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'video_id' => 'integer',
|
||||
];
|
||||
|
||||
public function video(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Video::class);
|
||||
}
|
||||
}
|
||||
23
app/Models/VideoPlay.php
Executable file
23
app/Models/VideoPlay.php
Executable file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class VideoPlay extends Model
|
||||
{
|
||||
public const UPDATED_AT = null;
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = ['user_id' => 'integer', 'video_id' => 'integer'];
|
||||
|
||||
public function scopeForCurrentUser(Builder $builder): Builder
|
||||
{
|
||||
if (Auth::check()) {
|
||||
return $builder->where('user_id', Auth::id());
|
||||
} else {
|
||||
return $builder->where('ip', getIp());
|
||||
}
|
||||
}
|
||||
}
|
||||
15
app/Models/VideoReport.php
Executable file
15
app/Models/VideoReport.php
Executable file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class VideoReport extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'comment_id' => 'integer',
|
||||
];
|
||||
}
|
||||
9
app/Models/VideoVote.php
Executable file
9
app/Models/VideoVote.php
Executable file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Common\Votes\Vote;
|
||||
|
||||
class VideoVote extends Vote
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user