400 lines
13 KiB
PHP
Executable File
400 lines
13 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Actions\Plays;
|
|
|
|
use App\Actions\Album;
|
|
use App\Actions\TrackPlay;
|
|
use App\Models\Episode;
|
|
use App\Models\Movie;
|
|
use App\Models\Season;
|
|
use App\Models\Series;
|
|
use App\Models\Title;
|
|
use App\Models\User;
|
|
use App\Models\Video;
|
|
use App\Models\VideoPlay;
|
|
use Common\Core\Values\ValueLists;
|
|
use Common\Database\Metrics\MetricDateRange;
|
|
use Common\Database\Metrics\Partition;
|
|
use Common\Database\Metrics\Trend;
|
|
use Exception;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Gate;
|
|
|
|
class BuildPlaysReport
|
|
{
|
|
protected Builder $builder;
|
|
protected array $params = [];
|
|
protected MetricDateRange $dateRange;
|
|
protected int $modelId;
|
|
|
|
public function execute(array $params): array
|
|
{
|
|
$this->params = $params;
|
|
$this->builder = $this->createBuilder();
|
|
|
|
$this->dateRange = new MetricDateRange(
|
|
start: $this->params['startDate'] ?? null,
|
|
end: $this->params['endDate'] ?? null,
|
|
timezone: $this->params['timezone'] ?? null,
|
|
);
|
|
|
|
$metrics = explode(',', Arr::get($params, 'metrics', 'plays'));
|
|
|
|
return collect($metrics)
|
|
->mapWithKeys(function ($metric) {
|
|
if ($metric === 'movies') {
|
|
return ['movies' => $this->getTitlesMetric(false)];
|
|
} elseif ($metric === 'series') {
|
|
return ['series' => $this->getTitlesMetric(true)];
|
|
} else {
|
|
$method = sprintf('get%sMetric', ucfirst($metric));
|
|
if (method_exists($this, $method)) {
|
|
return [$metric => $this->$method()];
|
|
}
|
|
return [$metric => []];
|
|
}
|
|
})
|
|
->toArray();
|
|
}
|
|
|
|
protected function createBuilder(): Builder
|
|
{
|
|
$model = Arr::get($this->params, 'model', '');
|
|
$parts = explode('=', $model);
|
|
|
|
// might send track_play=0, check if variable is set, instead of being truthy
|
|
if (!isset($parts[0]) || !isset($parts[1])) {
|
|
$parts = ['video_play', 0];
|
|
}
|
|
|
|
$model = modelTypeToNamespace($parts[0]);
|
|
$this->modelId = (int) $parts[1];
|
|
|
|
switch ($model) {
|
|
case VideoPlay::class:
|
|
// all plays, not scoped to any resource (for admin area)
|
|
Gate::authorize('admin.access');
|
|
$builder = VideoPlay::query();
|
|
break;
|
|
case Video::class:
|
|
$video = Video::findOrFail($this->modelId);
|
|
Gate::authorize('update', $video);
|
|
$builder = $video->plays()->getQuery();
|
|
break;
|
|
case Movie::class:
|
|
case Series::class:
|
|
case Title::class:
|
|
$title = Title::findOrFail($this->modelId);
|
|
Gate::authorize('update', $title);
|
|
$builder = $title->plays()->getQuery();
|
|
break;
|
|
case Season::class:
|
|
$season = Season::with(['title'])->findOrFail($this->modelId);
|
|
Gate::authorize('update', $season->title);
|
|
$builder = VideoPlay::join(
|
|
'videos',
|
|
'video_plays.video_id',
|
|
'=',
|
|
'videos.id',
|
|
)
|
|
->join('titles', 'videos.title_id', '=', 'titles.id')
|
|
->where('titles.id', $season->title_id)
|
|
->where('videos.season_num', $season->number);
|
|
break;
|
|
case Episode::class:
|
|
$episode = Episode::with(['title'])->findOrFail($this->modelId);
|
|
Gate::authorize('update', $episode->title);
|
|
$builder = $episode->plays()->getQuery();
|
|
break;
|
|
default:
|
|
throw new Exception();
|
|
}
|
|
|
|
return $builder;
|
|
}
|
|
|
|
protected function getPlaysMetric(): array
|
|
{
|
|
if (config('common.site.fake_plays_data')) {
|
|
$data = (new GenerateFakePlaysData())->playsTrend(
|
|
$this->builder,
|
|
$this->dateRange,
|
|
);
|
|
} else {
|
|
$data = (new Trend(
|
|
$this->builder,
|
|
dateRange: $this->dateRange,
|
|
))->count();
|
|
}
|
|
|
|
return [
|
|
'granularity' => $this->dateRange->granularity,
|
|
'total' => array_sum(Arr::pluck($data, 'value')),
|
|
'datasets' => [
|
|
[
|
|
'label' => __('Plays'),
|
|
'data' => $data,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
protected function getDevicesMetric(): array
|
|
{
|
|
return $this->getPartitionMetric('device', 5);
|
|
}
|
|
|
|
protected function getBrowsersMetric(): array
|
|
{
|
|
return $this->getPartitionMetric('browser', 8);
|
|
}
|
|
|
|
protected function getPlatformsMetric(): array
|
|
{
|
|
return $this->getPartitionMetric('platform', 5);
|
|
}
|
|
|
|
protected function getTitlesMetric(bool $isSeries = null): array
|
|
{
|
|
if (config('common.site.fake_plays_data')) {
|
|
$data = (new GenerateFakePlaysData())->titles($isSeries);
|
|
} else {
|
|
$data = (new Partition(
|
|
$this->builder
|
|
->join('videos', 'video_plays.video_id', '=', 'videos.id')
|
|
->join('titles', 'videos.title_id', '=', 'titles.id')
|
|
->when(
|
|
!is_null($isSeries),
|
|
fn($query) => $query->where('is_series', $isSeries),
|
|
)
|
|
->orderBy('aggregate', 'desc'),
|
|
groupBy: 'title_id',
|
|
dateRange: $this->dateRange,
|
|
limit: 30,
|
|
))->count();
|
|
|
|
$titles = Title::whereIn('id', Arr::pluck($data, 'label'))
|
|
->compact()
|
|
->get();
|
|
|
|
$data = array_map(function ($item) use ($titles) {
|
|
$title = $titles->firstWhere('id', $item['label']);
|
|
$item['model'] = $title;
|
|
$item['label'] = $title->name;
|
|
return $item;
|
|
}, $data);
|
|
}
|
|
|
|
return [
|
|
'datasets' => [
|
|
[
|
|
'label' => __('Plays'),
|
|
'data' => $data,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
protected function getSeasonsMetric(): array
|
|
{
|
|
if (config('common.site.fake_plays_data')) {
|
|
$data = (new GenerateFakePlaysData())->seasons($this->modelId);
|
|
} else {
|
|
$data = (new Partition(
|
|
$this->builder
|
|
->whereNotNull('season_num')
|
|
->orderBy('aggregate', 'desc'),
|
|
groupBy: 'season_num',
|
|
dateRange: $this->dateRange,
|
|
limit: 30,
|
|
))->count();
|
|
|
|
$seasons = Season::where('title_id', $this->modelId)
|
|
->whereIn('number', Arr::pluck($data, 'label'))
|
|
->with(['title' => fn($query) => $query->compact()])
|
|
->get();
|
|
|
|
$data = array_map(function ($item) use ($seasons) {
|
|
$season = $seasons->firstWhere('number', (int) $item['label']);
|
|
$item['model'] = $season;
|
|
$item['label'] = __('Season :number', [
|
|
'number' => $season->number,
|
|
]);
|
|
return $item;
|
|
}, $data);
|
|
}
|
|
|
|
return [
|
|
'datasets' => [
|
|
[
|
|
'label' => __('Plays'),
|
|
'data' => $data,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
protected function getEpisodesMetric(): array
|
|
{
|
|
if (config('common.site.fake_plays_data')) {
|
|
$data = (new GenerateFakePlaysData())->episodes($this->modelId);
|
|
} else {
|
|
$data = (new Partition(
|
|
$this->builder
|
|
->whereNotNull('episode_id')
|
|
->orderBy('aggregate', 'desc'),
|
|
groupBy: 'episode_id',
|
|
dateRange: $this->dateRange,
|
|
limit: 30,
|
|
))->count();
|
|
|
|
$episodes = Episode::whereIn('id', Arr::pluck($data, 'label'))
|
|
->with(['title' => fn($query) => $query->compact()])
|
|
->get();
|
|
|
|
$data = array_map(function ($item) use ($episodes) {
|
|
$episode = $episodes->firstWhere('id', (int) $item['label']);
|
|
$item['model'] = $episode;
|
|
$item['label'] = __('Season :s, Episode :e', [
|
|
's' => $episode->season_number,
|
|
'e' => $episode->number,
|
|
]);
|
|
return $item;
|
|
}, $data);
|
|
}
|
|
|
|
return [
|
|
'datasets' => [
|
|
[
|
|
'label' => __('Plays'),
|
|
'data' => $data,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
protected function getVideosMetric(): array
|
|
{
|
|
if (config('common.site.fake_plays_data')) {
|
|
$data = (new GenerateFakePlaysData())->videos();
|
|
} else {
|
|
$data = (new Partition(
|
|
$this->builder->orderBy('aggregate', 'desc'),
|
|
groupBy: 'video_id',
|
|
dateRange: $this->dateRange,
|
|
limit: 30,
|
|
))->count();
|
|
|
|
$videos = Video::whereIn('id', Arr::pluck($data, 'label'))
|
|
->with(['title' => fn($query) => $query->compact()])
|
|
->get();
|
|
|
|
$data = array_map(function ($item) use ($videos) {
|
|
$video = $videos->firstWhere('id', $item['label']);
|
|
$item['model'] = $video;
|
|
$item['label'] = $video?->name;
|
|
return $item;
|
|
}, $data);
|
|
}
|
|
|
|
$data = array_values(array_filter($data, fn($item) => $item['label']));
|
|
|
|
return [
|
|
'datasets' => [
|
|
[
|
|
'label' => __('Plays'),
|
|
'data' => $data,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
protected function getUsersMetric(): array
|
|
{
|
|
if (config('common.site.fake_plays_data')) {
|
|
$data = (new GenerateFakePlaysData())->users();
|
|
} else {
|
|
$data = (new Partition(
|
|
$this->builder->orderBy('aggregate', 'desc'),
|
|
groupBy: 'user_id',
|
|
dateRange: $this->dateRange,
|
|
limit: 30,
|
|
))->count();
|
|
|
|
$userIds = collect($data)
|
|
->pluck('label')
|
|
->filter()
|
|
->unique();
|
|
$users = User::whereIn('id', $userIds)->get();
|
|
|
|
$data = array_map(function ($item) use ($users) {
|
|
$user =
|
|
$users->firstWhere('id', $item['label']) ??
|
|
new User(['first_name' => __('Guest user')]);
|
|
$item['model'] = $user;
|
|
$item['label'] = $user->display_name;
|
|
return $item;
|
|
}, $data);
|
|
}
|
|
|
|
return [
|
|
'datasets' => [
|
|
[
|
|
'label' => __('Plays'),
|
|
'data' => $data,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
protected function getLocationsMetric(): array
|
|
{
|
|
$metric = $this->getPartitionMetric('location');
|
|
|
|
$countries = app(ValueLists::class)->countries();
|
|
|
|
$metric['datasets'][0]['data'] = array_map(function ($location) use (
|
|
$countries,
|
|
$metric,
|
|
) {
|
|
// only short country code is stored in DB, get and return full country name as well
|
|
$location['code'] = strtolower($location['label']);
|
|
$location['label'] =
|
|
Arr::first(
|
|
$countries,
|
|
fn($country) => strtolower($country['code']) ===
|
|
strtolower($location['code']),
|
|
)['name'] ?? $location['label'];
|
|
return $location;
|
|
}, $metric['datasets'][0]['data']);
|
|
|
|
return $metric;
|
|
}
|
|
|
|
protected function getPartitionMetric(
|
|
string $groupBy,
|
|
int $limit = 10,
|
|
): array {
|
|
if (config('common.site.fake_plays_data')) {
|
|
$data = (new GenerateFakePlaysData())->partitionMetric($groupBy);
|
|
} else {
|
|
$data = (new Partition(
|
|
$this->builder,
|
|
groupBy: $groupBy,
|
|
dateRange: $this->dateRange,
|
|
limit: $limit,
|
|
))->count();
|
|
}
|
|
|
|
return [
|
|
'datasets' => [
|
|
[
|
|
'label' => __('Plays'),
|
|
'data' => $data,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
}
|