38
app/Actions/Titles/DeleteSeasons.php
Executable file
38
app/Actions/Titles/DeleteSeasons.php
Executable file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Season;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DeleteSeasons
|
||||
{
|
||||
public function execute(array|Collection $seasonIds): void
|
||||
{
|
||||
// seasons
|
||||
DB::table('creditables')
|
||||
->whereIn('creditable_id', $seasonIds)
|
||||
->where('creditable_type', Season::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
app(Season::class)
|
||||
->whereIn('id', $seasonIds)
|
||||
->delete();
|
||||
|
||||
// episodes
|
||||
$episodeIds = app(Episode::class)
|
||||
->whereIn('season_id', $seasonIds)
|
||||
->pluck('id');
|
||||
|
||||
DB::table('creditables')
|
||||
->whereIn('creditable_id', $episodeIds)
|
||||
->where('creditable_type', Episode::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
app(Episode::class)
|
||||
->whereIn('id', $episodeIds)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
104
app/Actions/Titles/DeleteTitles.php
Executable file
104
app/Actions/Titles/DeleteTitles.php
Executable file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Image;
|
||||
use App\Models\Listable;
|
||||
use App\Models\Review;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use App\Models\Video;
|
||||
use Common\Comments\Comment;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DeleteTitles
|
||||
{
|
||||
public function execute(array $titleIds): void
|
||||
{
|
||||
$seasonIds = app(Season::class)
|
||||
->whereIn('title_id', $titleIds)
|
||||
->pluck('id');
|
||||
app(DeleteSeasons::class)->execute($seasonIds);
|
||||
|
||||
// credits
|
||||
DB::table('creditables')
|
||||
->whereIn('creditable_id', $titleIds)
|
||||
->where('creditable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// images
|
||||
app(Image::class)
|
||||
->whereIn('model_id', $titleIds)
|
||||
->where('model_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// list items
|
||||
app(Listable::class)
|
||||
->whereIn('listable_id', $titleIds)
|
||||
->where('listable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// channel items
|
||||
DB::table('channelables')
|
||||
->whereIn('channelable_id', $titleIds)
|
||||
->where('channelable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// reviews
|
||||
Review::whereIn('reviewable_id', $titleIds)
|
||||
->where('reviewable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// comments
|
||||
Comment::whereIn('commentable_id', $titleIds)
|
||||
->where('commentable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// tags
|
||||
DB::table('taggables')
|
||||
->whereIn('taggable_id', $titleIds)
|
||||
->where('taggable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// keywords
|
||||
DB::table('keyword_title')
|
||||
->whereIn('title_id', $titleIds)
|
||||
->delete();
|
||||
|
||||
// countries
|
||||
DB::table('country_title')
|
||||
->whereIn('title_id', $titleIds)
|
||||
->delete();
|
||||
|
||||
// genres
|
||||
DB::table('genre_title')
|
||||
->whereIn('title_id', $titleIds)
|
||||
->delete();
|
||||
|
||||
// videos
|
||||
$videoIds = app(Video::class)
|
||||
->whereIn('title_id', $titleIds)
|
||||
->pluck('id');
|
||||
app(Video::class)
|
||||
->whereIn('id', $videoIds)
|
||||
->delete();
|
||||
|
||||
DB::table('video_votes')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->delete();
|
||||
DB::table('video_captions')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->delete();
|
||||
DB::table('video_reports')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->delete();
|
||||
DB::table('video_plays')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->delete();
|
||||
|
||||
// titles
|
||||
Title::withoutGlobalScope('adult')
|
||||
->whereIn('id', $titleIds)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
40
app/Actions/Titles/HandlesEncodedTmdbId.php
Executable file
40
app/Actions/Titles/HandlesEncodedTmdbId.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
trait HandlesEncodedTmdbId
|
||||
{
|
||||
public static function encodeTmdbId(
|
||||
string $provider,
|
||||
string $type,
|
||||
string|int $id,
|
||||
): string {
|
||||
return base64_encode("$provider|$type|$id");
|
||||
}
|
||||
|
||||
public static function decodeTmdbId($encodedId): array
|
||||
{
|
||||
$encodedId = base64_decode($encodedId);
|
||||
$parts = explode('|', $encodedId);
|
||||
if (count($parts) === 3) {
|
||||
return [
|
||||
'provider' => $parts[0],
|
||||
'type' => $parts[1],
|
||||
'id' => $parts[2],
|
||||
];
|
||||
} else {
|
||||
return ['provider' => null, 'type' => null, 'id' => null];
|
||||
}
|
||||
}
|
||||
|
||||
public static function decodeTmdbIdOrFail(string $encodedId): array
|
||||
{
|
||||
[$provider, $type, $id] = array_values(
|
||||
static::decodeTmdbId($encodedId),
|
||||
);
|
||||
if (!$provider || !$type || !$id) {
|
||||
abort(404);
|
||||
}
|
||||
return [(int) $id, $type];
|
||||
}
|
||||
}
|
||||
75
app/Actions/Titles/HasCreditableRelation.php
Executable file
75
app/Actions/Titles/HasCreditableRelation.php
Executable file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Person;
|
||||
use DB;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
trait HasCreditableRelation
|
||||
{
|
||||
public function credits(): BelongsToMany
|
||||
{
|
||||
$query = $this->morphToMany(Person::class, 'creditable')->withPivot([
|
||||
'id',
|
||||
'job',
|
||||
'department',
|
||||
'order',
|
||||
'character',
|
||||
]);
|
||||
|
||||
$query = $query->select(['people.id', 'name', 'poster']);
|
||||
|
||||
// order by department first, so we always get director,
|
||||
// writers and creators, even if limit is applied to this query
|
||||
$prefix = DB::getTablePrefix();
|
||||
return $query
|
||||
->orderBy(
|
||||
DB::raw(
|
||||
"FIELD(department, 'directing', 'creators', 'writing', 'actors')",
|
||||
),
|
||||
)
|
||||
// should be "desc" and not "asc" because "minus" is added which will reverse order
|
||||
->orderBy(DB::raw("-{$prefix}creditables.order"), 'desc');
|
||||
}
|
||||
|
||||
public function updateCredit(int $pivotId, array $payload): void
|
||||
{
|
||||
// lowercase payload
|
||||
$payload = collect($payload)
|
||||
->mapWithKeys(function ($value, $key) {
|
||||
if ($key === 'department' || $key === 'job') {
|
||||
$value = strtolower($value);
|
||||
}
|
||||
return [$key => $value];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
$this->credits()
|
||||
->newPivotQuery()
|
||||
->where('id', $pivotId)
|
||||
->update($payload);
|
||||
}
|
||||
|
||||
public function createCredit(array $payload): void
|
||||
{
|
||||
// lowercase payload
|
||||
$payload = collect($payload)
|
||||
->mapWithKeys(function ($value, $key) {
|
||||
if ($key === 'department' || $key === 'job') {
|
||||
$value = strtolower($value);
|
||||
}
|
||||
return [$key => $value];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
if ($payload['department'] === 'actors') {
|
||||
$payload['order'] =
|
||||
$this->credits()
|
||||
->wherePivot('department', 'actors')
|
||||
->count() + 1;
|
||||
}
|
||||
|
||||
$this->credits()->attach($payload['person_id'], $payload);
|
||||
}
|
||||
}
|
||||
56
app/Actions/Titles/HasVideoRelation.php
Executable file
56
app/Actions/Titles/HasVideoRelation.php
Executable file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
trait HasVideoRelation
|
||||
{
|
||||
public function videos(): HasMany
|
||||
{
|
||||
return $this->hasMany(Video::class)->applySelectedSort();
|
||||
}
|
||||
|
||||
public function primaryVideo(): HasOne
|
||||
{
|
||||
$preferFull = settings('streaming.prefer_full');
|
||||
return $this->hasOne(Video::class)
|
||||
->when($preferFull, function ($query) {
|
||||
$query->where('category', 'full');
|
||||
})
|
||||
->select([
|
||||
'id',
|
||||
'title_id',
|
||||
'name',
|
||||
'category',
|
||||
'episode_id',
|
||||
'season_num',
|
||||
'episode_num',
|
||||
])
|
||||
->when(
|
||||
// is series or movie and not specific episode
|
||||
fn() => static::class !== Episode::class && $preferFull,
|
||||
function ($builder) {
|
||||
return $builder->where(function ($builder) {
|
||||
// video attached directly to movie or series
|
||||
$builder
|
||||
->where(
|
||||
fn($builder) => $builder
|
||||
->whereNull('season_num')
|
||||
->whereNull('episode_num'),
|
||||
)
|
||||
// video attached to first episode of series
|
||||
->orWhere(
|
||||
fn($builder) => $builder
|
||||
->where('season_num', 1)
|
||||
->where('episode_num', 1),
|
||||
);
|
||||
});
|
||||
},
|
||||
)
|
||||
->applySelectedSort();
|
||||
}
|
||||
}
|
||||
53
app/Actions/Titles/InsertsTmdbTitleOrPerson.php
Executable file
53
app/Actions/Titles/InsertsTmdbTitleOrPerson.php
Executable file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Person;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
trait InsertsTmdbTitleOrPerson
|
||||
{
|
||||
public function insertOrRetrieve(Collection $tmdbItems): Collection
|
||||
{
|
||||
$tmdbItems = $tmdbItems->map(function ($value) {
|
||||
unset($value['relation_data']);
|
||||
unset($value['model_type']);
|
||||
unset($value['id']);
|
||||
return $value;
|
||||
});
|
||||
|
||||
$select =
|
||||
static::class === Person::class
|
||||
? ['id', 'tmdb_id', 'name']
|
||||
: ['id', 'tmdb_id', 'name', 'is_series'];
|
||||
|
||||
$existing = $this->withoutGlobalScope('adult')
|
||||
->select($select)
|
||||
->whereIn('tmdb_id', $tmdbItems->pluck('tmdb_id'))
|
||||
->get()
|
||||
->mapWithKeys(fn($item) => [$item['tmdb_id'] => $item]);
|
||||
|
||||
$new = $tmdbItems
|
||||
->filter(fn($item) => !isset($existing[$item['tmdb_id']]))
|
||||
->values();
|
||||
|
||||
if ($new->isNotEmpty()) {
|
||||
$new->transform(function ($item) {
|
||||
$item['created_at'] = Arr::get(
|
||||
$item,
|
||||
'created_at',
|
||||
Carbon::now(),
|
||||
);
|
||||
return $item;
|
||||
});
|
||||
$this->insert($new->toArray());
|
||||
return $this->whereIn('tmdb_id', $tmdbItems->pluck('tmdb_id'))
|
||||
->select($select)
|
||||
->get();
|
||||
} else {
|
||||
return $existing;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
app/Actions/Titles/LoadSeasonEpisodeNumbers.php
Executable file
18
app/Actions/Titles/LoadSeasonEpisodeNumbers.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class LoadSeasonEpisodeNumbers
|
||||
{
|
||||
public function execute(int $titleId, int $seasonNumber): array
|
||||
{
|
||||
return DB::table('episodes')
|
||||
->where('title_id', $titleId)
|
||||
->where('season_number', $seasonNumber)
|
||||
->orderBy('episode_number', 'asc')
|
||||
->pluck('episode_number')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
54
app/Actions/Titles/Retrieve/GetRelatedTitles.php
Executable file
54
app/Actions/Titles/Retrieve/GetRelatedTitles.php
Executable file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GetRelatedTitles
|
||||
{
|
||||
public function execute(Title $title, array $params = []): Collection
|
||||
{
|
||||
$titleIds = $this->getByTags($title, $params);
|
||||
|
||||
if ($titleIds->isNotEmpty()) {
|
||||
return Title::whereIn('id', $titleIds)
|
||||
->with(['primaryVideo'])
|
||||
->when(isset($params['compact']), fn($q) => $q->compact())
|
||||
->get();
|
||||
}
|
||||
|
||||
return collect();
|
||||
}
|
||||
|
||||
private function getByTags(Title $title, array $params): Collection
|
||||
{
|
||||
$prefix = DB::getTablePrefix();
|
||||
$keywordIds = $title->keywords->pluck('id');
|
||||
$genreIds = $title->genres->pluck('id');
|
||||
|
||||
return DB::table('titles')
|
||||
->join('keyword_title as k', 'k.title_id', '=', 'titles.id')
|
||||
->join('genre_title as g', 'g.title_id', '=', 'titles.id')
|
||||
->select(
|
||||
DB::raw(
|
||||
"{$prefix}titles.id, COUNT({$prefix}k.id) + COUNT({$prefix}g.id) as total_count",
|
||||
),
|
||||
)
|
||||
->whereIn('k.keyword_id', $keywordIds)
|
||||
->whereIn('g.genre_id', $genreIds)
|
||||
->when($title->release_date, function ($q) use ($title) {
|
||||
$q->whereBetween('release_date', [
|
||||
$title->release_date->subYears(5)->format('Y-m-d'),
|
||||
$title->release_date->addYears(5)->format('Y-m-d'),
|
||||
]);
|
||||
})
|
||||
->where('titles.id', '!=', $title->id)
|
||||
->groupBy('titles.id')
|
||||
->orderBy('total_count', 'desc')
|
||||
->limit(Arr::get($params, 'limit', 10))
|
||||
->pluck('titles.id');
|
||||
}
|
||||
}
|
||||
40
app/Actions/Titles/Retrieve/PaginateSeasonEpisodes.php
Executable file
40
app/Actions/Titles/Retrieve/PaginateSeasonEpisodes.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PaginateSeasonEpisodes
|
||||
{
|
||||
public function execute(
|
||||
Title $title,
|
||||
int $seasonNumber,
|
||||
array $params = [],
|
||||
): AbstractPaginator {
|
||||
$builder = $title->episodes()->where('season_number', $seasonNumber);
|
||||
$builder->with(['primaryVideo']);
|
||||
|
||||
$orderBy = Arr::get($params, 'orderBy', 'episode_number');
|
||||
$orderDir = Arr::get($params, 'orderDir', 'asc');
|
||||
|
||||
$pagination = $builder
|
||||
->orderBy($orderBy, $orderDir)
|
||||
->simplePaginate(Arr::get($params, 'perPage', 30));
|
||||
|
||||
// only show first paragraph of description
|
||||
$pagination->through(function ($episode) use ($params) {
|
||||
$episode->description = explode("\n", $episode->description)[0];
|
||||
if (Arr::get($params, 'excludeDescription')) {
|
||||
$episode->description = null;
|
||||
} elseif (Arr::get($params, 'truncateDescriptions')) {
|
||||
$episode->description = Str::limit($episode->description, 200);
|
||||
}
|
||||
return $episode;
|
||||
});
|
||||
|
||||
return $pagination;
|
||||
}
|
||||
}
|
||||
26
app/Actions/Titles/Retrieve/PaginateTitleSeasons.php
Executable file
26
app/Actions/Titles/Retrieve/PaginateTitleSeasons.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class PaginateTitleSeasons
|
||||
{
|
||||
public function execute(Title $title, array $params = []): AbstractPaginator
|
||||
{
|
||||
return $title
|
||||
->seasons()
|
||||
->select([
|
||||
'seasons.id',
|
||||
'seasons.poster',
|
||||
'seasons.release_date',
|
||||
'number',
|
||||
'title_id',
|
||||
])
|
||||
->withCount('episodes')
|
||||
->orderBy('number', 'desc')
|
||||
->paginate(Arr::get($params, 'perPage', 8));
|
||||
}
|
||||
}
|
||||
54
app/Actions/Titles/Retrieve/PaginateTitles.php
Executable file
54
app/Actions/Titles/Retrieve/PaginateTitles.php
Executable file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PaginateTitles
|
||||
{
|
||||
public function execute(array $params, $builder = null): AbstractPaginator
|
||||
{
|
||||
if (!$builder) {
|
||||
$builder = Title::query();
|
||||
}
|
||||
|
||||
if ($type = Arr::get($params, 'type')) {
|
||||
$builder->where('is_series', $type === 'series');
|
||||
}
|
||||
|
||||
$builder->with(['primaryVideo']);
|
||||
|
||||
$datasource = new Datasource($builder, $params);
|
||||
|
||||
// prevent duplicate items when ordering by columns that are not
|
||||
// guaranteed to be unique (budget, popularity , revenue etc.)
|
||||
$datasource->secondaryOrderCol = 'id';
|
||||
|
||||
// only show titles with more than 50 votes when filtering by rating
|
||||
if ($datasource->filters->has('tmdb_vote_average')) {
|
||||
$builder->where('tmdb_vote_count', '>=', 50);
|
||||
}
|
||||
|
||||
$order = $datasource->getOrder();
|
||||
|
||||
$order['col'] = str_replace(
|
||||
['user_score', 'rating'],
|
||||
config('common.site.rating_column'),
|
||||
$order['col'],
|
||||
);
|
||||
|
||||
// show titles with less than 50 votes on tmdb last, regardless of their average
|
||||
if (Str::contains($order['col'], 'tmdb_vote_average')) {
|
||||
$builder->orderBy(DB::raw('tmdb_vote_count > 100'), 'desc');
|
||||
}
|
||||
|
||||
$datasource->order = $order;
|
||||
|
||||
return $datasource->paginate();
|
||||
}
|
||||
}
|
||||
55
app/Actions/Titles/Retrieve/ShowTitle.php
Executable file
55
app/Actions/Titles/Retrieve/ShowTitle.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ShowTitle
|
||||
{
|
||||
public function execute(int|string $id, array $params): array
|
||||
{
|
||||
if (defined('SHOULD_PRERENDER')) {
|
||||
$params['skipUpdating'] = true;
|
||||
$params['load'] =
|
||||
'images,genres,productionCountries,keywords,videos,primaryVideo,seasons,compactCredits';
|
||||
$params['loadCount'] = 'seasons';
|
||||
}
|
||||
|
||||
if (is_numeric($id) || ctype_digit($id)) {
|
||||
$title = Title::findOrFail($id);
|
||||
} else {
|
||||
$title = Title::firstOrCreateFromEncodedTmdbId($id);
|
||||
}
|
||||
|
||||
if (!Arr::get($params, 'skipUpdating')) {
|
||||
$title = $title->maybeUpdateFromExternal();
|
||||
if (!$title) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
$response = ['title' => $title->loadCount('seasons')];
|
||||
|
||||
foreach (explode(',', Arr::get($params, 'load', '')) as $relation) {
|
||||
$methodName = sprintf('load%s', Str::camel($relation));
|
||||
if (method_exists($this, $methodName)) {
|
||||
$response = $this->$methodName($title, $params, $response);
|
||||
} elseif (method_exists($title, $relation)) {
|
||||
$title->load($relation);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (
|
||||
explode(',', Arr::get($params, 'loadCount', ''))
|
||||
as $relation
|
||||
) {
|
||||
if (method_exists($title, $relation)) {
|
||||
$title->loadCount($relation);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
209
app/Actions/Titles/Store/StoreCredits.php
Executable file
209
app/Actions/Titles/Store/StoreCredits.php
Executable file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Store;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Person;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StoreCredits
|
||||
{
|
||||
public const UNIQUE_KEY = 'tmdb_id';
|
||||
|
||||
private Title|Episode|Season|Person|null $model = null;
|
||||
|
||||
public function __construct(private Person $person, private Title $title)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $originalMediaItems
|
||||
*/
|
||||
public function execute(Title|Episode|Season|Person $model, $originalMediaItems)
|
||||
{
|
||||
$this->model = $model;
|
||||
|
||||
if (empty($originalMediaItems)) return;
|
||||
|
||||
// generate records we will insert into "creditables" pivot table
|
||||
$newPivotRecords = $this->generatePivotRecords($originalMediaItems);
|
||||
if ($newPivotRecords->isEmpty()) return;
|
||||
|
||||
// fetch all existing "creditable" pivot rows for this creditable
|
||||
$existingPivotRecords = $this->getExistingRecords($newPivotRecords);
|
||||
|
||||
// merge new and existing pivot records, so "order" column value is not lost
|
||||
$mergedPivotRecords = $this->mergeNewAndExistingRecords($existingPivotRecords, $newPivotRecords);
|
||||
|
||||
// delete all "creditables" pivot table records for this creditable
|
||||
$this->detachExistingRecords($existingPivotRecords);
|
||||
|
||||
// insert new pivot records
|
||||
DB::table('creditables')->insert($mergedPivotRecords->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $originalMediaItems
|
||||
* @return Collection
|
||||
*/
|
||||
private function generatePivotRecords($originalMediaItems)
|
||||
{
|
||||
// need to remove series, otherwise it will attach all guest starts to main series, because
|
||||
// tmdb API does not specify whether credit belongs to series, season or episode.
|
||||
$originalMediaItems = $this->filterOutSeries($originalMediaItems);
|
||||
$dbMediaItems = $this->insertOrRetrieveMediaItems($originalMediaItems);
|
||||
|
||||
return collect($originalMediaItems)->map(function($originalMediaItem) use($dbMediaItems) {
|
||||
$creditable = $dbMediaItems->first(fn($item, $uniqueKey) => $uniqueKey === $originalMediaItem[self::UNIQUE_KEY] && $item['is_series'] === Arr::get($originalMediaItem, 'is_series'));
|
||||
|
||||
if ( ! $creditable) return null;
|
||||
|
||||
// either attaching multiple titles to a person
|
||||
// or attaching multiple people to title/season/episode
|
||||
if ($this->model->model_type === Person::MODEL_TYPE) {
|
||||
$personId = $this->model->id;
|
||||
$creditableId = $creditable->id;
|
||||
} else {
|
||||
$personId = $creditable->id;
|
||||
$creditableId = $this->model->id;
|
||||
}
|
||||
|
||||
// build relation records for attaching all credits to title
|
||||
// (same person might be attached to title multiple times)
|
||||
return [
|
||||
'id' => null,
|
||||
'person_id' => $personId,
|
||||
'creditable_id' => $creditableId,
|
||||
'creditable_type' => $this->getCreditableType(),
|
||||
'character' => Arr::get($originalMediaItem, 'relation_data.character'),
|
||||
'order' => Arr::get($originalMediaItem, 'relation_data.order'),
|
||||
'department' => Arr::get($originalMediaItem, 'relation_data.department'),
|
||||
'job' => Arr::get($originalMediaItem, 'relation_data.job'),
|
||||
];
|
||||
})->filter()->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge existing "creditables" pivot table records with the ones
|
||||
* we are about to insert. This needs to be done because sometimes
|
||||
* "order" property does not exist on "new" records, but exists on
|
||||
* "old" ones and since we will delete "old" ones, "order" would be lost.
|
||||
*
|
||||
* @param Collection $existingRecords
|
||||
* @param Collection $newRecords
|
||||
* @return Collection
|
||||
*/
|
||||
private function mergeNewAndExistingRecords($existingRecords, $newRecords)
|
||||
{
|
||||
// all of these properties on both arrays must match for them to be considered equal
|
||||
$matchProps = collect(['person_id', 'creditable_id', 'creditable_type', 'character', 'department', 'job']);
|
||||
|
||||
return $newRecords->map(function($newRecord) use($existingRecords, $matchProps) {
|
||||
$oldRecord = $existingRecords->first(fn($existingRecord) => $matchProps->every(fn($prop) => $existingRecord[$prop] === $newRecord[$prop]), []);
|
||||
return $this->arrayMergeIfNotNull($newRecord, $oldRecord);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $arr1
|
||||
* @param array $arr2
|
||||
* @return array
|
||||
*/
|
||||
private function arrayMergeIfNotNull($arr1, $arr2) {
|
||||
foreach($arr2 as $key => $val) {
|
||||
$is_set_and_not_null = isset($arr1[$key]);
|
||||
if ( $val == null && $is_set_and_not_null ) {
|
||||
$arr2[$key] = $arr1[$key];
|
||||
}
|
||||
}
|
||||
return array_merge($arr1, $arr2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or retrieve titles or people that need to be attached.
|
||||
*
|
||||
* @param Collection|(Title|Person)[] $mediaItems
|
||||
* @return Collection
|
||||
*/
|
||||
private function insertOrRetrieveMediaItems(Collection|array $mediaItems)
|
||||
{
|
||||
// make sure we only insert person/title once, even
|
||||
// if they appear multiple time in title credits
|
||||
$uniqueMediaItems = collect($mediaItems)->unique(self::UNIQUE_KEY)->values();
|
||||
|
||||
if ($uniqueMediaItems->isEmpty()) return collect();
|
||||
|
||||
if ($uniqueMediaItems[0]['model_type'] === Person::MODEL_TYPE) {
|
||||
$mediaItems = $this->person->insertOrRetrieve($uniqueMediaItems);
|
||||
} else {
|
||||
$mediaItems = $this->title->insertOrRetrieve($uniqueMediaItems);
|
||||
}
|
||||
|
||||
return $mediaItems->mapWithKeys(fn($item) => [$item[self::UNIQUE_KEY] => $item]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $records
|
||||
* @return Collection
|
||||
*/
|
||||
private function getExistingRecords($records)
|
||||
{
|
||||
// select only fields needed to do the diff
|
||||
return DB::table('creditables')
|
||||
->whereIn('person_id', $records->pluck('person_id'))
|
||||
->whereIn('job', $records->pluck('job'))
|
||||
->where('creditable_type', $records[0]['creditable_type'])
|
||||
->whereIn('creditable_id', $records->pluck('creditable_id'))
|
||||
->get()
|
||||
->map(fn($record) => (array) $record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete creditable records that already exist.
|
||||
*
|
||||
* @param Collection $records
|
||||
*/
|
||||
private function detachExistingRecords($records)
|
||||
{
|
||||
if ($records->isEmpty()) return;
|
||||
|
||||
// select only fields needed to do the diff
|
||||
$existingRecords = DB::table('creditables')
|
||||
->whereIn('person_id', $records->pluck('person_id'))
|
||||
->whereIn('job', $records->pluck('job'))
|
||||
->where('creditable_type', $records[0]['creditable_type'])
|
||||
->whereIn('creditable_id', $records->pluck('creditable_id'))
|
||||
->get();
|
||||
|
||||
DB::table('creditables')->whereIn('id', $existingRecords->pluck('id'))->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get creditable type for morphToMany or morphedByMany relation.
|
||||
*/
|
||||
private function getCreditableType(): string
|
||||
{
|
||||
if ($this->model::class === Person::class) {
|
||||
// won't be attaching seasons or episodes here
|
||||
// so can just return title type instantly
|
||||
return Title::MODEL_TYPE;
|
||||
} else {
|
||||
return $this->model::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all series and episodes from specified array.
|
||||
*
|
||||
* @param array $originalMediaItems
|
||||
* @return array
|
||||
*/
|
||||
private function filterOutSeries($originalMediaItems)
|
||||
{
|
||||
return array_filter($originalMediaItems, fn($creditable) => !Arr::get($creditable, 'is_series'));
|
||||
}
|
||||
}
|
||||
51
app/Actions/Titles/Store/StoreEpisodeData.php
Executable file
51
app/Actions/Titles/Store/StoreEpisodeData.php
Executable file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Store;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class StoreEpisodeData
|
||||
{
|
||||
public function execute(Title $title, Season $season, $episodes): Title
|
||||
{
|
||||
$existingEpisodes = $season->episodes()->get();
|
||||
foreach ($episodes as $episodeData) {
|
||||
$episode = $this->storePrimaryData(
|
||||
$season,
|
||||
$existingEpisodes,
|
||||
$episodeData,
|
||||
);
|
||||
app(StoreCredits::class)->execute($episode, $episodeData['cast']);
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
|
||||
private function storePrimaryData(
|
||||
Season $season,
|
||||
Collection $existingEpisodes,
|
||||
array $episodeData,
|
||||
): Episode {
|
||||
$episodeData = array_filter(
|
||||
$episodeData,
|
||||
fn($value) => !is_array($value) && $value !== Episode::MODEL_TYPE,
|
||||
);
|
||||
$episodeData['title_id'] = $season->title_id;
|
||||
unset($episodeData['id']);
|
||||
|
||||
$existingEpisode = $existingEpisodes->firstWhere(
|
||||
'episode_number',
|
||||
$episodeData['episode_number'],
|
||||
);
|
||||
|
||||
if ($existingEpisode) {
|
||||
$existingEpisode->update($episodeData);
|
||||
return $existingEpisode;
|
||||
} else {
|
||||
return $season->episodes()->create($episodeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
app/Actions/Titles/Store/StoreSeasonData.php
Executable file
56
app/Actions/Titles/Store/StoreSeasonData.php
Executable file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Store;
|
||||
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
|
||||
class StoreSeasonData
|
||||
{
|
||||
private ?Title $title = null;
|
||||
|
||||
public function execute(Title $title, array $data): Title
|
||||
{
|
||||
if (empty($data)) {
|
||||
return $title;
|
||||
}
|
||||
|
||||
$this->title = $title;
|
||||
|
||||
$season = $this->persistData($data);
|
||||
if (isset($data['cast'])) {
|
||||
app(StoreCredits::class)->execute($season, $data['cast']);
|
||||
}
|
||||
|
||||
if (isset($data['episodes'])) {
|
||||
app(StoreEpisodeData::class)->execute(
|
||||
$title,
|
||||
$season,
|
||||
$data['episodes'],
|
||||
);
|
||||
}
|
||||
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
private function persistData(array $data): Season
|
||||
{
|
||||
// remove all relation data
|
||||
$data = array_filter(
|
||||
$data,
|
||||
fn($value) => !is_array($value) && $value !== Season::MODEL_TYPE,
|
||||
);
|
||||
|
||||
// if season data did not change then timestamps
|
||||
// will not be updated because model is not dirty
|
||||
$data['updated_at'] = now();
|
||||
|
||||
return Season::updateOrCreate(
|
||||
[
|
||||
'title_id' => $this->title->id,
|
||||
'number' => $data['number'],
|
||||
],
|
||||
$data,
|
||||
);
|
||||
}
|
||||
}
|
||||
141
app/Actions/Titles/Store/StoreTitleData.php
Executable file
141
app/Actions/Titles/Store/StoreTitleData.php
Executable file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Store;
|
||||
|
||||
use App\Actions\Titles\StoresMediaImages;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StoreTitleData
|
||||
{
|
||||
use StoresMediaImages;
|
||||
|
||||
private ?Title $title = null;
|
||||
|
||||
private ?array $data = null;
|
||||
|
||||
private ?array $options = null;
|
||||
|
||||
public function execute(
|
||||
Title $title,
|
||||
array $data,
|
||||
array $options = [],
|
||||
): Title {
|
||||
$this->title = $title;
|
||||
$this->data = $data;
|
||||
$this->options = $options;
|
||||
|
||||
$this->persistData();
|
||||
$this->persistRelations();
|
||||
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
private function persistData(): void
|
||||
{
|
||||
$titleData = array_filter(
|
||||
$this->data,
|
||||
fn(
|
||||
$value, // make sure we don't overwrite existing values with null
|
||||
) => !is_array($value) &&
|
||||
($this->options['overrideWithEmptyValues'] ?? !is_null($value)),
|
||||
);
|
||||
|
||||
$this->title->fill($titleData)->save();
|
||||
}
|
||||
|
||||
private function persistRelations(): void
|
||||
{
|
||||
$relations = array_filter($this->data, fn($value) => is_array($value));
|
||||
|
||||
foreach ($relations as $name => $values) {
|
||||
switch ($name) {
|
||||
case 'videos':
|
||||
$this->persistVideos($values);
|
||||
break;
|
||||
case 'images':
|
||||
$this->storeImages($values, $this->title);
|
||||
break;
|
||||
case 'genres':
|
||||
$this->persistTags($values, 'genre');
|
||||
break;
|
||||
case 'countries':
|
||||
$this->persistTags($values, 'production_country');
|
||||
break;
|
||||
case 'cast':
|
||||
app(StoreCredits::class)->execute($this->title, $values);
|
||||
break;
|
||||
case 'keywords':
|
||||
$this->persistTags($values, 'keyword');
|
||||
break;
|
||||
case 'seasons':
|
||||
$this->persistSeasons($values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function persistSeasons(array $seasons): void
|
||||
{
|
||||
$newSeasons = collect($seasons)
|
||||
->map(function ($season) {
|
||||
$season['title_id'] = $this->title->id;
|
||||
return $season;
|
||||
})
|
||||
->filter(
|
||||
fn($season) => !$this->title->seasons->contains(
|
||||
'number',
|
||||
$season['number'],
|
||||
),
|
||||
);
|
||||
|
||||
if ($newSeasons->isNotEmpty()) {
|
||||
Season::insert($newSeasons->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
private function persistTags(array $tags, string $type): void
|
||||
{
|
||||
$values = collect($tags)->map(
|
||||
fn($tag) => [
|
||||
'name' => $tag['name'],
|
||||
'display_name' => Arr::get(
|
||||
$tag,
|
||||
'display_name',
|
||||
ucfirst($tag['name']),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
$tags = app(modelTypeToNamespace($type))->insertOrRetrieve(
|
||||
$values,
|
||||
$type,
|
||||
);
|
||||
|
||||
$relation = $this->title->{Str::camel(Str::plural($type))}();
|
||||
$relation->syncWithoutDetaching($tags->pluck('id'));
|
||||
}
|
||||
|
||||
private function persistVideos(array $values): void
|
||||
{
|
||||
$videos = collect($values)
|
||||
->unique(fn($v) => strtolower($v['name']))
|
||||
->values()
|
||||
->map(function ($value, $i) {
|
||||
$value['title_id'] = $this->title->id;
|
||||
$value['order'] = $i + 1;
|
||||
$value['created_at'] = now();
|
||||
$value['updated_at'] = now();
|
||||
return $value;
|
||||
});
|
||||
|
||||
Video::where('origin', '!=', 'local')
|
||||
->where('title_id', $this->title->id)
|
||||
->whereNull('episode_num')
|
||||
->delete();
|
||||
|
||||
Video::insert($videos->toArray());
|
||||
}
|
||||
}
|
||||
47
app/Actions/Titles/StoreMediaImageOnDisk.php
Executable file
47
app/Actions/Titles/StoreMediaImageOnDisk.php
Executable file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class StoreMediaImageOnDisk
|
||||
{
|
||||
// sizes should be ordered by size (desc), to avoid blurry images
|
||||
private array $sizes = [
|
||||
'original' => null,
|
||||
'large' => 500,
|
||||
'medium' => 300,
|
||||
'small' => 92,
|
||||
];
|
||||
|
||||
public function execute(UploadedFile $file): string
|
||||
{
|
||||
$hash = Str::random(30);
|
||||
|
||||
$manager = new ImageManager(new Driver());
|
||||
$img = $manager->read($file);
|
||||
|
||||
$extension = $file->extension() ?? 'jpeg';
|
||||
|
||||
foreach ($this->sizes as $key => $size) {
|
||||
if ($size) {
|
||||
$img->scale($size);
|
||||
}
|
||||
|
||||
Storage::disk('public')->put(
|
||||
"media-images/backdrops/$hash/$key.$extension",
|
||||
$extension === 'png' ? $img->toPng() : $img->toJpeg(),
|
||||
);
|
||||
}
|
||||
|
||||
$endpoint = config('common.site.file_preview_endpoint');
|
||||
$uri = "media-images/backdrops/$hash/original.$extension";
|
||||
return $endpoint
|
||||
? "$endpoint/storage/$uri"
|
||||
: Storage::disk('public')->url($uri);
|
||||
}
|
||||
}
|
||||
24
app/Actions/Titles/StoresMediaImages.php
Executable file
24
app/Actions/Titles/StoresMediaImages.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Person;
|
||||
use App\Models\Title;
|
||||
|
||||
trait StoresMediaImages
|
||||
{
|
||||
public function storeImages(array $values, Title|Person $model): void
|
||||
{
|
||||
$values = array_map(function ($value) use ($model) {
|
||||
$value['model_id'] = $model->id;
|
||||
$value['model_type'] = $model->getMorphClass();
|
||||
return $value;
|
||||
}, $values);
|
||||
|
||||
$model
|
||||
->images()
|
||||
->where('source', '!=', 'local')
|
||||
->delete();
|
||||
$model->images()->insert($values);
|
||||
}
|
||||
}
|
||||
164
app/Actions/Titles/TitleCredits.php
Executable file
164
app/Actions/Titles/TitleCredits.php
Executable file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Person;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TitleCredits
|
||||
{
|
||||
public function loadCompact(
|
||||
Title $title,
|
||||
Season $season = null,
|
||||
Episode $episode = null,
|
||||
): Collection {
|
||||
$builder = $this->getBaseQuery($title, $season, $episode)
|
||||
->whereIn('creditables.department', [
|
||||
'actors',
|
||||
'writing',
|
||||
'directing',
|
||||
'creators',
|
||||
])
|
||||
->limit(50);
|
||||
|
||||
$credits = $this->groupByDepartment($builder);
|
||||
|
||||
if (isset($credits['actors'])) {
|
||||
$credits['actors'] = $credits['actors']->take(15)->values();
|
||||
}
|
||||
if (isset($credits['writing'])) {
|
||||
$credits['writing'] = $credits['writing']->take(3)->values();
|
||||
}
|
||||
if (isset($credits['directing'])) {
|
||||
$credits['directing'] = $credits['directing']
|
||||
->filter(fn($c) => $c['pivot']['job'] === 'director')
|
||||
->take(3)
|
||||
->values();
|
||||
}
|
||||
if (isset($credits['creators'])) {
|
||||
$credits['creators'] = $credits['creators']->take(3)->values();
|
||||
}
|
||||
|
||||
return $credits;
|
||||
}
|
||||
|
||||
public function loadFull(
|
||||
Title $title,
|
||||
Season $season = null,
|
||||
Episode $episode = null,
|
||||
): Collection {
|
||||
$builder = $this->getBaseQuery($title, $season, $episode);
|
||||
return $this->groupByDepartment($builder);
|
||||
}
|
||||
|
||||
protected function getBaseQuery(
|
||||
Title $title = null,
|
||||
Season $season = null,
|
||||
Episode $episode = null,
|
||||
): Builder {
|
||||
$prefix = DB::getTablePrefix();
|
||||
$builder = Person::join(
|
||||
'creditables',
|
||||
'people.id',
|
||||
'=',
|
||||
'creditables.person_id',
|
||||
)
|
||||
->select([
|
||||
'people.id',
|
||||
'people.name',
|
||||
'people.poster',
|
||||
DB::raw("{$prefix}creditables.id as pivot_id"),
|
||||
DB::raw(
|
||||
"{$prefix}creditables.creditable_id as pivot_creditable_id",
|
||||
),
|
||||
DB::raw(
|
||||
"{$prefix}creditables.creditable_type as pivot_creditable_type",
|
||||
),
|
||||
DB::raw("{$prefix}creditables.job as pivot_job"),
|
||||
DB::raw("{$prefix}creditables.department as pivot_department"),
|
||||
DB::raw("{$prefix}creditables.order as pivot_order"),
|
||||
DB::raw("{$prefix}creditables.character as pivot_character"),
|
||||
])
|
||||
// Need to keep same person in different departments (as actor and producer for example)
|
||||
// while still removing duplicates when loading credits for title, season and episode
|
||||
->groupBy(['people.id', 'creditables.department']);
|
||||
|
||||
$builder->where(function ($q) use ($title, $season, $episode) {
|
||||
if ($title) {
|
||||
$q->orWhere(
|
||||
fn($q) => $q
|
||||
->where('creditables.creditable_id', $title->id)
|
||||
->where(
|
||||
'creditables.creditable_type',
|
||||
$title->getMorphClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($episode) {
|
||||
$q->orWhere(
|
||||
fn($q) => $q
|
||||
->where('creditables.creditable_id', $episode->id)
|
||||
->where(
|
||||
'creditables.creditable_type',
|
||||
$episode->getMorphClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($season) {
|
||||
$q->orWhere(
|
||||
fn($q) => $q
|
||||
->where('creditables.creditable_id', $season->id)
|
||||
->where(
|
||||
'creditables.creditable_type',
|
||||
$season->getMorphClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// order by department first, so we always get director,
|
||||
// writers and creators, even if limit is applied to this query
|
||||
$prefix = DB::getTablePrefix();
|
||||
return $builder
|
||||
->orderBy(
|
||||
DB::raw(
|
||||
"FIELD(department, 'directing', 'creators', 'writing', 'actors')",
|
||||
),
|
||||
)
|
||||
// should be "desc" and not "asc" because "minus" is added which will reverse order
|
||||
->orderBy(DB::raw("-{$prefix}creditables.order"), 'desc');
|
||||
}
|
||||
|
||||
protected function groupByDepartment(Builder $builder): Collection
|
||||
{
|
||||
$credits = $builder->get()->map(function ($credit) {
|
||||
$credit->pivot = [
|
||||
'id' => $credit->pivot_id,
|
||||
'job' => $credit->pivot_job,
|
||||
'department' => $credit->pivot_department,
|
||||
'character' => $credit->pivot_character,
|
||||
];
|
||||
|
||||
unset(
|
||||
$credit->pivot_id,
|
||||
$credit->pivot_creditable_id,
|
||||
$credit->pivot_creditable_type,
|
||||
$credit->pivot_job,
|
||||
$credit->pivot_department,
|
||||
$credit->pivot_order,
|
||||
$credit->pivot_character,
|
||||
);
|
||||
|
||||
return $credit;
|
||||
});
|
||||
|
||||
return $credits->groupBy('pivot.department');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user