first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

193
app/Actions/AppValueLists.php Executable file
View File

@@ -0,0 +1,193 @@
<?php
namespace App\Actions;
use App\Models\Genre;
use App\Models\Keyword;
use App\Models\ProductionCountry;
use App\Models\Title;
use Common\Core\Values\ValueLists;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class AppValueLists extends ValueLists
{
public function titleFilterLanguages($params = [])
{
$allLanguages = json_decode(
$this->fs->get(base_path('resources/lists/tmdb-languages.json')),
true,
);
$usedLanguages = Title::withoutGlobalScope('adult')
->select('language')
->groupBy('language')
->select([DB::raw('count(*) as total'), 'language'])
->pluck('total', 'language')
->toArray();
return collect($allLanguages)
->filter(fn($language) => isset($usedLanguages[$language['code']]))
->map(
fn($language) => [
'value' => $language['code'],
'name' => $language['name'],
'total' => $usedLanguages[$language['code']] ?? 0,
],
)
->sortByDesc('total')
->values();
}
public function productionCountries($params = []): Collection
{
return ProductionCountry::get(['id', 'name', 'display_name'])->map(
fn($country) => [
'value' => $country->id,
'name' => $country->display_name,
],
);
}
public function genres($params = []): Collection
{
if (Arr::get($params, 'type') === 'tmdb') {
$genres = json_decode(
$this->fs->get(resource_path('lists/tmdb-genres.json')),
true,
);
return collect($genres)->map(
fn($genre) => [
'value' => $genre['id'],
'name' => $genre['name'],
],
);
} else {
return Genre::get(['id', 'name', 'display_name'])->map(
fn($genre) => [
'value' => $genre->id,
'name' => $genre->display_name,
],
);
}
}
public function languages($params = [])
{
if (Arr::get($params, 'type') === 'tmdb') {
return collect(
json_decode(
$this->fs->get(resource_path('lists/tmdb-languages.json')),
true,
),
);
} else {
return parent::languages();
}
}
public function countries($params = [])
{
if (Arr::get($params, 'type') === 'tmdb') {
return collect(
json_decode(
$this->fs->get(resource_path('lists/tmdb-countries.json')),
true,
),
);
} else {
return parent::countries();
}
}
public function keywords($params = [])
{
if (Arr::get($params, 'type') === 'tmdb') {
$allKeywords = collect(
json_decode(
$this->fs->get(resource_path('lists/tmdb-keywords.json')),
true,
),
);
if (isset($params['searchQuery'])) {
$allKeywords = $allKeywords
->filter(
fn($keyword) => str_contains(
strtolower($keyword['name']),
strtolower($params['searchQuery']),
),
)
->values();
}
$keywords = $allKeywords->slice(0, 50)->map(
fn($keyword) => [
'value' => $keyword['id'],
'name' => $keyword['name'],
],
);
if ($selectedValue = Arr::get($params, 'selectedValue')) {
$selectedKeyword = $allKeywords->firstWhere(
'id',
$selectedValue,
);
if ($selectedKeyword) {
$keywords->prepend([
'value' => $selectedKeyword['id'],
'name' => $selectedKeyword['name'],
]);
}
}
} else {
$query = isset($params['searchQuery'])
? Keyword::search($params['searchQuery'])
: Keyword::query();
$keywords = $query
->take(50)
->get()
->map(
fn($keyword) => [
'value' => $keyword->id,
'name' => $keyword->display_name,
],
);
if ($selectedValue = Arr::get($params, 'selectedValue')) {
$selectedKeyword = Keyword::find($selectedValue);
if ($selectedKeyword) {
$keywords->prepend([
'value' => $selectedKeyword->id,
'name' => $selectedKeyword->display_name,
]);
}
}
}
return $keywords;
}
public function titleFilterAgeRatings($params = []): Collection
{
return Title::withoutGlobalScope('adult')
->select('certification')
->groupBy('certification')
->select([DB::raw('count(*) as total'), 'certification'])
->orderBy('total', 'desc')
->pluck('certification')
->filter(
fn($certification) => !!$certification &&
$certification !== '-',
)
->map(
fn($certification) => [
'value' => $certification,
'name' => strtoupper($certification),
],
)
->values();
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Actions\Channels;
use App\Models\Episode;
use App\Models\Genre;
use App\Models\Keyword;
use App\Models\Person;
use App\Models\ProductionCountry;
use App\Models\Title;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class FetchContentFromLocalDatabase
{
public function execute(
string $method,
string $modelType,
?array $filters = [],
): Collection {
$isSeries = $modelType === Title::SERIES_TYPE;
if ($method === 'trendingPeople') {
return Person::orderBy('popularity', 'desc')
->limit(20)
->get();
} elseif ($method === 'mostPopular') {
return $this->getTitleBuilder($filters)
->orderBy('popularity', 'desc')
->limit(20)
->where('is_series', $isSeries)
->get();
} elseif ($method === 'topRated') {
return $this->getTopRatedTitles(
$this->getTitleBuilder($filters),
$isSeries,
);
} elseif ($method === 'upcoming') {
return $this->getMoviesReleasingBetween(
$this->getTitleBuilder($filters),
Carbon::now(),
Carbon::now()->addWeek(),
);
} elseif ($method === 'nowPlaying') {
return $this->getMoviesReleasingBetween(
$this->getTitleBuilder($filters),
Carbon::now(),
Carbon::now()->subWeek(),
);
} elseif ($method === 'onTheAir') {
$this->getSeriesAiringBetween(
$this->getTitleBuilder($filters),
Carbon::now(),
Carbon::now()->addWeek(),
);
} elseif ($method === 'airingToday') {
return $this->getSeriesAiringBetween(
$this->getTitleBuilder($filters),
Carbon::now(),
Carbon::now()->addDay(),
);
} elseif ($method === 'latestVideos') {
return $this->getTitleBuilder($filters)
->join('videos', 'titles.id', '=', 'videos.title_id')
->where('videos.origin', 'local')
->where('approved', true)
->orderBy('videos.created_at', 'desc')
->select('titles.*')
->distinct()
->limit(20)
->get();
} elseif ($method === 'lastAdded') {
return $this->getTitleBuilder($filters)
::where('is_series', $isSeries)
->orderBy('created_at', 'desc')
->limit(20)
->get();
}
}
private function getTopRatedTitles(mixed $builder, $isSeries): Collection
{
$ratingCol = config('common.site.rating_column');
$query = $builder->where('is_series', $isSeries);
if (Str::contains($ratingCol, 'tmdb_vote_average')) {
$query->orderBy(DB::raw('tmdb_vote_count > 100'), 'desc');
}
return $query
->orderBy($ratingCol, 'desc')
->limit(20)
->get();
}
private function getMoviesReleasingBetween(
mixed $builder,
$from,
$to,
): Collection {
return $builder
->whereBetween('release_date', [$from, $to])
->orderBy('popularity', 'desc')
->limit(20)
->where('is_series', false)
->get(['id', 'name']);
}
private function getSeriesAiringBetween(
mixed $builder,
$from,
$to,
): Collection {
$titleIds = Episode::whereBetween('release_date', [$from, $to])
->limit(300)
->get(['title_id'])
->pluck('title_id')
->unique()
->slice(0, 20);
return $builder->whereIn('id', $titleIds)->get(['id', 'name']);
}
private function getTitleBuilder(?array $filters = []): mixed
{
if (isset($filters['keyword'])) {
return Keyword::findOrFail($filters['keyword'])->titles();
} elseif (isset($filters['genre'])) {
return Genre::findOrFail($filters['genre'])->titles();
} elseif (isset($filters['country'])) {
return ProductionCountry::findOrFail($filters['country'])->titles();
} else {
return Title::query();
}
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\Actions\Channels;
use App\Models\Person;
use App\Models\Title;
use App\Services\Data\Tmdb\TmdbApi;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class FetchContentFromTmdb
{
public function execute(
string $method,
string $modelType,
?array $filters = [],
): Collection {
Title::disableSearchSyncing();
Person::disableSearchSyncing();
if ($modelType === Person::MODEL_TYPE) {
return $this->importPeople($method);
} else {
return $this->ImportTitles($method, $modelType, $filters);
}
}
protected function importPeople(string $method)
{
if ($method === 'trendingPeople') {
$people = app(TmdbApi::class)->trendingPeople();
return $people
->map(
fn($personData) => Person::firstOrCreateFromEncodedTmdbId(
$personData['id'],
)->maybeUpdateFromExternal([
'forceAutomation' => true,
]),
)
->filter();
}
}
protected function ImportTitles(
string $method,
string $modelType,
?array $extraFilters = [],
) {
$filters = $this->buildFilters($method, $modelType);
if (count($extraFilters)) {
if (isset($extraFilters['keyword'])) {
$filters['with_keywords'] = $extraFilters['keyword'];
}
if (isset($extraFilters['genre'])) {
$filters['with_genres'] = $extraFilters['genre'];
}
if (isset($extraFilters['country'])) {
$filters['with_origin_country'] = $extraFilters['country'];
}
if (isset($extraFilters['language'])) {
$filters['with_original_language'] = $extraFilters['language'];
}
}
// fetch from tmdb
$titles = app(TmdbApi::class)->browse(1, $modelType, $filters)[
'results'
];
// store in local db
return $titles
->map(function ($titleData) {
return Title::firstOrCreateFromEncodedTmdbId(
$titleData['id'],
)->maybeUpdateFromExternal([
'forceAutomation' => true,
//'updateLast3Seasons' => true,
]);
})
->filter();
}
protected function buildFilters(string $method, string $modelType): array
{
$type = "{$modelType}.{$method}";
switch ($type) {
case 'movie.mostPopular':
$from = Carbon::now()
->subMonths(6)
->format('Y-m-d');
return [
'sort_by' => 'popularity.desc',
'primary_release_date.gte' => $from,
];
case 'movie.topRated':
case 'series.topRated':
return [
'sort_by' => 'vote_average.desc',
'vote_count.gte' => 600,
];
case 'movie.upcoming':
$from = Carbon::now()
->addDay()
->format('Y-m-d');
$to = Carbon::now()
->addMonth()
->format('Y-m-d');
return [
'sort_by' => 'popularity.desc',
'with_release_type' => '2|3',
'primary_release_date.gte' => $from,
'primary_release_date.lte' => $to,
];
case 'movie.nowPlaying':
$from = Carbon::now()
->subMonths(2)
->format('Y-m-d');
$to = Carbon::now()
->subDays(2)
->format('Y-m-d');
return [
'sort_by' => 'popularity.desc',
'with_release_type' => '2|3',
'primary_release_date.gte' => $from,
'primary_release_date.lte' => $to,
];
case 'series.mostPopular':
return [
'sort_by' => 'popularity.desc',
];
case 'series.airingThisWeek':
$from = Carbon::now()
->startOfDay()
->format('Y-m-d');
$to = Carbon::now()
->startOfDay()
->addDays(6)
->format('Y-m-d');
return [
'sort_by' => 'popularity.desc',
'air_date.gte' => $from,
'air_date.lte' => $to,
];
case 'series.airingToday':
$from = Carbon::now()
->startOfDay()
->format('Y-m-d');
$to = Carbon::now()
->endOfDay()
->format('Y-m-d');
return [
'sort_by' => 'popularity.desc',
'air_date.gte' => $from,
'air_date.lte' => $to,
];
default:
return [];
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Actions\Demo;
use App\Models\Episode;
use App\Models\Title;
use App\Models\Video;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Symfony\Component\Console\Output\ConsoleOutput;
class GenerateDemoAnimeVideos
{
public function execute(): void
{
$output = new ConsoleOutput();
$output->write('Generating demo anime videos... ', true);
$this->createTitleLinks();
$this->createEpisodeLinks();
}
private function createTitleLinks(): void
{
Title::where('is_series', false)
->select('id')
->whereDoesntHave('videos', function ($query) {
$query->where('category', 'full')->where('origin', 'local');
})
->chunkById(100, function (Collection $titles) {
$titleVideos = $titles
->pluck('id')
->map(fn($titleId) => $this->getVideosData($titleId))
->flatten(1);
Video::insert($titleVideos->toArray());
});
}
private function createEpisodeLinks(): void
{
Episode::whereDoesntHave('videos', function ($query) {
$query->where('category', 'full')->where('origin', 'local');
})->chunkById(100, function (Collection $episodes) {
$episodeVideos = $episodes
->map(
fn(Episode $episode) => $this->getVideosData(
$episode->title_id,
$episode,
),
)
->flatten(1);
Video::insert($episodeVideos->toArray());
});
}
private function getVideosData($titleId, Episode $episode = null): array
{
$sharedData = [
'category' => 'full',
'title_id' => $titleId,
'season_num' => $episode?->season_number,
'episode_num' => $episode?->episode_number,
'episode_id' => $episode?->id,
'origin' => 'local',
'approved' => true,
'updated_at' => Carbon::now(),
'user_id' => 1,
];
$urls = [
'https://www.youtube.com/embed/ByXuk9QqQkk',
'https://player.vimeo.com/video/208890816',
'https://www.dailymotion.com/embed/video/x4qi23d',
'https://www.youtube.com/embed/xU47nhruN-Q',
'https://google.com',
];
$languages = ['en', 'ru', 'fr', 'en', 'en'];
$videos = [];
for ($i = 0; $i <= 4; $i++) {
$num = $i + 1;
$videos[] = array_merge($sharedData, [
'name' => "Mirror $num",
'src' => $urls[$i],
'quality' => $i === 3 ? '4k' : 'hd',
'language' => $languages[$i],
'type' => $i === 4 ? 'external' : 'embed',
'upvotes' => rand(1, 200),
'downvotes' => rand(1, 30),
'created_at' => Carbon::now()->subDay(),
]);
}
return $videos;
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace App\Actions\Demo;
use App\Models\Episode;
use App\Models\Title;
use App\Models\User;
use Common\Files\Traits\HandlesEntryPaths;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\ConsoleOutput;
class GenerateDemoComments
{
use HandlesEntryPaths;
private Collection $users;
private int $currentId = 1;
public function execute(string $variant = null): void
{
DB::table('comment_reports')->truncate();
DB::table('comment_votes')->truncate();
$this->currentId = DB::table('comments')->max('id') + 1;
$demoEmails = collect(
json_decode(
file_get_contents(app_path('Actions/Demo/demo-users.json')),
true,
),
)->pluck('email');
$this->users = User::whereIn('email', $demoEmails)->get();
// movies
$this->generateFor(
Title::where('release_date', '<', now()->subDays(6))
->where('is_series', false)
->where('popularity', '>=', 10),
$variant === 'anime'
? app_path('Actions/Demo/demo-anime-comments.json')
: app_path('Actions/Demo/demo-movie-comments.json'),
);
// series
$this->generateFor(
Title::where('release_date', '<', now()->subDays(6))
->withCount('episodes')
->where('is_series', true)
->where('popularity', '>=', 10)
->has('episodes', '<', 400),
$variant === 'anime'
? app_path('Actions/Demo/demo-anime-comments.json')
: app_path('Actions/Demo/demo-series-comments.json'),
);
// episodes
$this->generateFor(
Episode::where('release_date', '<', now()->subDays(6))->whereHas(
'title',
function ($query) {
$query->where('popularity', '>=', 1);
},
),
$variant === 'anime'
? app_path('Actions/Demo/demo-anime-comments.json')
: app_path('Actions/Demo/demo-episode-comments.json'),
);
}
protected function generateFor(Builder $builder, string $path): void
{
$query = $builder->has('comments', '<', 40)->select('id');
$count = $query->count();
if (!$count) {
return;
}
$output = new ConsoleOutput();
$progressBar = new ProgressBar($output, $count);
$progressBar->start();
$output->write(
"Generating comments for {$builder->getModel()->getTable()}",
true,
);
$userSequence = new Sequence(...$this->users->pluck('id')->toArray());
$data = json_decode(file_get_contents($path), true);
$itemComments = collect($data)
->slice(0, rand(40, 118))
->map(function ($comment) use ($userSequence) {
$date = now()
->subMonth(rand(0, 2))
->subDays(rand(1, 12));
return [
'user_id' => $userSequence(),
'content' => $comment['comment'],
'upvotes' => rand(5, 200),
'downvotes' => rand(0, 20),
'reports_count' => rand(0, 3),
'created_at' => $date,
'updated_at' => $date,
];
});
$query
->lazyById(100)
->each(function ($item) use ($progressBar, $itemComments) {
$payload = $itemComments->map(function ($comment) use ($item) {
$data = array_merge($comment, [
'id' => $this->currentId,
'path' => $this->encodePath($this->currentId),
'commentable_id' => $item->id,
'commentable_type' => $item->getMorphClass(),
]);
$this->currentId++;
return $data;
});
DB::table('comments')->insert($payload->toArray());
$progressBar->advance();
});
$progressBar->finish();
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Actions\Demo;
use App\Models\Title;
use App\Models\User;
use Common\Files\Traits\HandlesEntryPaths;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\ConsoleOutput;
class GenerateDemoReviews
{
use HandlesEntryPaths;
private Collection $users;
public function execute(): void
{
DB::table('review_reports')->truncate();
DB::table('review_feedback')->truncate();
$demoEmails = collect(
json_decode(
file_get_contents(app_path('Actions/Demo/demo-users.json')),
true,
),
)->pluck('email');
$this->users = User::whereIn('email', $demoEmails)->get();
$this->generateFor('movie');
$this->generateFor('series');
}
protected function generateFor(string $type): void
{
$query = Title::where('is_series', $type == 'series')
->where('popularity', '>=', 10)
->where('release_date', '<', now()->subDays(6))
->has('reviews', '<', 40)
->select('id');
$count = $query->count();
if ($count === 0) {
return;
}
$output = new ConsoleOutput();
$progressBar = new ProgressBar($output, $count);
$progressBar->start();
$output->write(
"Generating reviews for {$query->getModel()->getTable()}",
true,
);
$userSequence = new Sequence(...$this->users->pluck('id')->toArray());
$data = json_decode(
file_get_contents(app_path("Actions/Demo/demo-$type-reviews.json")),
true,
);
$movieReviews = collect($data)
->slice(0, rand(40, $this->users->count()))
->map(function ($review) use ($userSequence) {
$date = now()
->subMonth(rand(0, 2))
->subDays(rand(1, 12));
return [
'user_id' => $userSequence(),
'title' => $review['title'],
'body' => $review['body'],
'score' => $review['score'],
'has_text' => true,
'helpful_count' => rand(10, 200),
'not_helpful_count' => rand(0, 20),
'created_at' => $date,
'updated_at' => $date,
];
});
$query
->lazyById(100)
->each(function ($movie) use ($movieReviews, $progressBar) {
$payload = $movieReviews->map(
fn($review) => array_merge($review, [
'reviewable_id' => $movie->id,
'reviewable_type' => Title::MODEL_TYPE,
]),
);
DB::table('reviews')->insert($payload->toArray());
$progressBar->advance();
});
$progressBar->finish();
}
}

View File

@@ -0,0 +1,185 @@
<?php
namespace App\Actions\Demo;
use App\Models\Episode;
use App\Models\Title;
use App\Models\Video;
use App\Models\VideoCaption;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Symfony\Component\Console\Output\ConsoleOutput;
class GenerateDemoStreamVideos
{
protected array $demoVideos = [
[
'name' => 'Demo HLS',
'src' =>
'https://stream.mux.com/buWV5Srtc9vUQTWmgpTFkDNlA3zImB7C9CQ2CVgkZpI.m3u8',
'type' => 'stream',
'quality' => '1080p',
],
[
'name' => 'Demo HLS',
'src' =>
'https://stream.mux.com/Rus46I1a5LSATgJ6YDhbDo024F7qHEYZBTbOxRZJdIn4.m3u8',
'type' => 'stream',
'quality' => '1080p',
],
[
'name' => 'Demo HLS',
'src' =>
'https://stream.mux.com/E7hs5dNds85023rGvL4rrAjPI02BpRE2024XyFnjx007oj4.m3u8',
'type' => 'stream',
'quality' => '1080p',
],
[
'name' => 'Demo HLS',
'src' =>
'https://stream.mux.com/Q20238p74cLNL4Bm2ivdjGnSozST6Gec2Ym1EuDnCMJI.m3u8',
'type' => 'stream',
'quality' => '1080p',
],
[
'name' => 'Demo HLS',
'src' =>
'https://stream.mux.com/spmRS3NLqWHkkBGEYM4KRhj6Hytnat00AnPS8v3hj9qA.m3u8',
'type' => 'stream',
'quality' => '1080p',
],
[
'name' => 'Demo WebM',
'src' => 'storage/demo-videos/sprite-fright/sprite-fright.webm',
'type' => 'video',
'quality' => '1080p',
'captions' => [
[
'name' => 'English',
'language' => 'en',
'url' =>
'storage/demo-videos/sprite-fright/subs/sprite-fright.en.vtt',
],
[
'name' => 'Russian',
'language' => 'ru',
'url' =>
'storage/demo-videos/sprite-fright/subs/sprite-fright.ru.vtt',
],
[
'name' => 'German',
'language' => 'de',
'url' =>
'storage/demo-videos/sprite-fright/subs/sprite-fright.de.vtt',
],
[
'name' => 'Italian',
'language' => 'it',
'url' =>
'storage/demo-videos/sprite-fright/subs/sprite-fright.it.vtt',
],
],
],
];
protected Sequence $demoVideoSequence;
public function execute(): void
{
$this->demoVideoSequence = new Sequence(...$this->demoVideos);
$output = new ConsoleOutput();
$output->write('Generating demo stream videos... ', true);
$this->createVideosForAllMovies();
$this->createEpisodeVideos();
$this->createCaptions();
}
private function createVideosForAllMovies(): void
{
Title::where('is_series', false)
->whereDoesntHave(
'videos',
fn($query) => $query
->where('origin', 'local')
->where('category', 'full'),
)
->select('id')
->chunkById(100, function (Collection $titles) {
$titleVideos = $titles->map(
fn($title) => $this->getVideoData($title->id),
);
Video::insert($titleVideos->toArray());
});
}
private function createCaptions(): void
{
collect($this->demoVideos)
->filter(fn($video) => isset($video['captions']))
->each(function ($data) {
Video::where('origin', 'local')
->where('category', 'full')
->where('name', $data['name'])
->whereDoesntHave('captions')
->chunkById(100, function (Collection $videos) use ($data) {
$captionPayload = $videos->flatMap(
fn(Video $video) => array_map(
fn($captionData) => [
'name' => $captionData['name'],
'language' => $captionData['language'],
'url' => $captionData['url'],
'video_id' => $video->id,
'created_at' => now(),
'updated_at' => now(),
],
$data['captions'],
),
);
VideoCaption::insert($captionPayload->toArray());
});
});
}
private function createEpisodeVideos(): void
{
Episode::whereDoesntHave(
'videos',
fn($query) => $query
->where('origin', 'local')
->where('category', 'full'),
)->chunkById(100, function (Collection $episodes) {
$episodeVideos = $episodes->map(
fn(Episode $episode) => $this->getVideoData(
$episode->title_id,
$episode,
),
);
Video::insert($episodeVideos->toArray());
});
}
private function getVideoData(int $titleId, Episode $episode = null): array
{
$sequence = $this->demoVideoSequence;
$data = $sequence();
return [
'name' => $data['name'],
'src' => $data['src'],
'type' => $data['type'],
'category' => 'full',
'quality' => $data['quality'],
'title_id' => $titleId,
'season_num' => $episode?->season_number,
'episode_num' => $episode?->episode_number,
'episode_id' => $episode?->id,
'origin' => 'local',
'approved' => true,
'created_at' => now(),
'updated_at' => now(),
'language' => 'en',
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Actions\Demo;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Support\Str;
use Symfony\Component\Console\Output\ConsoleOutput;
class GenerateDemoUsers
{
public function execute(): void
{
$data = collect(
json_decode(
file_get_contents(
base_path('app/Actions/Demo/demo-users.json'),
),
true,
),
)->unique('email');
$existing = User::whereIn('email', $data->pluck('email'))->get();
$unique = $data->reject(
fn($user) => $existing->contains('email', $user['email']),
);
if ($unique->isEmpty()) {
return;
}
$output = new ConsoleOutput();
$output->write('Generating demo users... ', true);
$avatarSequence = new Sequence(...range(1, 75));
$users = $unique->map(function ($user) use ($avatarSequence) {
$number = $avatarSequence();
$gender = strtolower($user['gender']);
return [
'email' => $user['email'],
'first_name' => $user['first_name'],
'last_name' => $user['last_name'],
'gender' => $gender,
'password' => Str::random(),
'email_verified_at' => now(),
'avatar' => "https://xsgames.co/randomusers/assets/avatars/$gender/$number.jpg",
];
});
User::insert($users->toArray());
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Actions\Demo;
use App\Models\Video;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\ConsoleOutput;
class GenerateDemoVideoVotes
{
public function execute(): void
{
// delete old votes
DB::table('video_votes')->truncate();
$count = Video::where('upvotes', '<', 60)->count();
if ($count == 0) {
return;
}
$output = new ConsoleOutput();
$output->write('Generating video votes... ', true);
$progressBar = new ProgressBar($output, $count);
$progressBar->start();
Video::select('id')
->where('upvotes', '<', 60)
->lazyById(100)
->each(function (Video $video) use ($progressBar) {
$video->update([
'upvotes' => rand(60, 2100),
'downvotes' => rand(50, 150),
]);
$progressBar->advance();
});
$progressBar->finish();
}
}

View File

@@ -0,0 +1,161 @@
[
{
"comment": "OMG, this anime had me on the edge of my seat! The action scenes were so freaking epic, and the plot was just mind-blowing."
},
{
"comment": "Dude, I can't get enough of the awesome animation in this series. It's like a visual feast, man!"
},
{
"comment": "This anime is a total blast, yo! It's got a bit of everything laughs, romance, and crazy adventures."
},
{
"comment": "Bro, just when I thought I had it all figured out, this anime threw me for a loop! Loved all those surprises, man."
},
{
"comment": "The character development in this anime is freaking awesome! I got super attached to the main crew and their struggles."
},
{
"comment": "No lie, I binge-watched this entire series in one sitting. It's hella addictive, worth staying up all night for!"
},
{
"comment": "If you want a feel-good anime with a positive message, this one's a winner. Left me with a big ol' smile, man!"
},
{
"comment": "The voice acting in this anime is off the charts, bro! The emotions hit you right in the gut."
},
{
"comment": "As an anime fanatic, I'm telling you, this one's the real deal. A hidden gem that needs more love, dude!"
},
{
"comment": "The world-building in this anime is sick, man! I got lost in that crazy universe, couldn't get enough of it!"
},
{
"comment": "Bro, the art style is out of this world! Every frame is like a work of art, totally blows your mind!"
},
{
"comment": "Ngl, I haven't cried this much over an anime in forever. The feels hit you right in the heart, dude!"
},
{
"comment": "This anime took me on an emotional rollercoaster, man! I laughed, I cried, I freaking loved it all!"
},
{
"comment": "The fight scenes are straight-up epic! They get your blood pumping, and you can't take your eyes off 'em."
},
{
"comment": "Man, I'm jamming to the sick tunes in this anime. The soundtrack is fire, sets the perfect mood!"
},
{
"comment": "If you're into mind-bending plots and crazy twists, you're gonna be mind-blown by this anime, bro!"
},
{
"comment": "The supporting characters in this anime are just as rad as the main crew. I dig 'em all!"
},
{
"comment": "This anime knows how to balance humor and serious stuff. Keeps you entertained the whole time!"
},
{
"comment": "This anime ain't afraid to tackle deep questions, man. Makes you think about life and stuff, you know?"
},
{
"comment": "Big props to this anime for showing respect to different cultures. It feels genuine, man."
},
{
"comment": "The pacing in this anime is on point, dude. It keeps the story moving without dragging it out."
},
{
"comment": "This anime's got a kickass mythology and lore. It adds a whole new dimension to the story!"
},
{
"comment": "The friendships in this anime are so heartwarming, man. It's all about the bonds, and it hits you in the feels!"
},
{
"comment": "Yo, I've been recommending this anime to everyone I know. It's a must-watch for any anime fan, no doubt!"
},
{
"comment": "OMG, the humor in this anime had me cracking up big time! Laughed my guts out in every episode!"
},
{
"comment": "I'm obsessed with the crazy character designs, man. Each one is so unique and stands out!"
},
{
"comment": "This anime is full of epic quotes that stick with you, dude. You'll be saying 'em all day long!"
},
{
"comment": "I wish I could live in the world of this anime, man. The universe is so freaking cool!"
},
{
"comment": "The character arcs in this anime are next-level, bro. You see 'em grow and change, and it's awesome!"
},
{
"comment": "The action sequences are out of this world, man. They get your heart racing, no lie!"
},
{
"comment": "Yo, this anime left me with a rollercoaster of emotions. Happy, sad, everything in between!"
},
{
"comment": "I can't get enough of the catchy opening and ending themes, man. They get stuck in your head!"
},
{
"comment": "The attention to detail in the animation is sick. It's all about those little things that make it pop!"
},
{
"comment": "The villain in this anime is wicked! Love to hate 'em, bro!"
},
{
"comment": "The character relationships feel so real and relatable, man. You get invested in their lives!"
},
{
"comment": "OMG, the plot twists had me shook, bro. I never saw 'em coming, that's for sure!"
},
{
"comment": "This anime dives deep into life's big questions, man. Makes you ponder and stuff."
},
{
"comment": "The art and animation quality never disappoint. It's top-notch all the way, dude!"
},
{
"comment": "The emotional depth of this anime hits you hard, man. It's a rollercoaster ride!"
},
{
"comment": "I couldn't stop watching once I started, dude. It's totally addicting!"
},
{
"comment": "This anime strikes the perfect balance between laughs and heartwarming moments. So darn enjoyable!"
},
{
"comment": "The character backstories are on point, bro. It adds layers of awesomeness to the story!"
},
{
"comment": "Yo, I'm impressed by how this anime respects different cultures. It's all about that diversity, man!"
},
{
"comment": "The world-building in this anime is so freaking cool. It's like a whole new universe to explore!"
},
{
"comment": "This anime's all about friendship, love, and courage, man. Hits you right in the feels!"
},
{
"comment": "The plot keeps you guessing, bro. It's like a wild ride with twists at every turn!"
},
{
"comment": "The character dynamics and chemistry are fire, man. It's a joy to watch 'em interact!"
},
{
"comment": "OMG, the art style is mind-blowing! It's like a feast for the eyes, dude!"
},
{
"comment": "I appreciate how this anime tackles real-world issues while still being hella entertaining."
},
{
"comment": "The soundtrack adds so much to the anime, bro. It sets the mood perfectly!"
},
{
"comment": "This anime got me right in the feels, man. Laughed, cried, and cheered for the characters!"
},
{
"comment": "The world-building and lore in this anime are insane, dude. It's a whole new dimension!"
},
{
"comment": "This anime left me with a mix of emotions from laughter to tears. Freaking unforgettable!"
}
]

View File

@@ -0,0 +1,365 @@
[
{
"comment": "OMG, this episode blew my mind! So many unexpected twists!"
},
{
"comment": "Just watched this episode and I'm speechless. It's a game-changer!"
},
{
"comment": "The suspense in this episode is unreal. Kept me on the edge of my seat!"
},
{
"comment": "This episode had me laughing out loud. So hilarious!"
},
{
"comment": "The emotional depth in this episode is incredible. It hit me right in the feels!"
},
{
"comment": "Just finished watching this episode and I'm in awe. So well-crafted!"
},
{
"comment": "The acting in this episode is phenomenal. Each performance is top-notch!"
},
{
"comment": "This episode had me hooked from start to finish. So captivating!"
},
{
"comment": "The plot development in this episode is mind-blowing. It's building up to something big!"
},
{
"comment": "Just witnessed the biggest cliffhanger in this episode. I need to know what happens next!"
},
{
"comment": "The chemistry between the characters in this episode is electric. I love their interactions!"
},
{
"comment": "This episode had me on an emotional rollercoaster. I laughed, I cried, I felt it all!"
},
{
"comment": "The cinematography in this episode is stunning. Each shot is visually breathtaking!"
},
{
"comment": "This episode is a rollercoaster ride of emotions. It's intense and heart-wrenching!"
},
{
"comment": "The dialogue in this episode is sharp and impactful. It kept me engaged!"
},
{
"comment": "This episode is a masterpiece. The writing, the acting, everything is on point!"
},
{
"comment": "The character growth in this episode is remarkable. They're evolving so beautifully!"
},
{
"comment": "This episode left me with so many questions. I can't wait for the next one!"
},
{
"comment": "The chemistry between the actors in this episode is undeniable. They have great synergy!"
},
{
"comment": "This episode is a rollercoaster of emotions. It had me on the edge of my seat!"
},
{
"comment": "The cinematography in this episode is stunning. It adds another layer of depth!"
},
{
"comment": "This episode is a turning point in the series. It's a game-changer!"
},
{
"comment": "The performances in this episode are outstanding. The actors brought their A-game!"
},
{
"comment": "This episode kept me guessing till the very end. So many surprises!"
},
{
"comment": "The writing in this episode is brilliant. The storytelling is top-notch!"
},
{
"comment": "This episode had me on an emotional rollercoaster. I'm still recovering!"
},
{
"comment": "The intensity in this episode is off the charts. It had me at the edge of my seat!"
},
{
"comment": "Just watched this episode and I'm shook. It took the series to a whole new level!"
},
{
"comment": "The cinematography in this episode is breathtaking. Each shot is a work of art!"
},
{
"comment": "This episode had me hooked from the first minute. Such a gripping storyline!"
},
{
"comment": "The character dynamics in this episode are so well-executed. I love seeing their relationships!"
},
{
"comment": "This episode is a rollercoaster of emotions. It made me laugh and cry!"
},
{
"comment": "The plot twists in this episode are mind-blowing. They kept me on my toes!"
},
{
"comment": "This episode left me speechless. It's a game-changer!"
},
{
"comment": "The acting in this episode is exceptional. The performances are so powerful!"
},
{
"comment": "This episode had me at the edge of my seat. So intense and suspenseful!"
},
{
"comment": "The storytelling in this episode is masterful. It's a captivating journey!"
},
{
"comment": "This episode had me in tears. It touched my heart in ways I didn't expect!"
},
{
"comment": "The chemistry between the characters in this episode is fire. I ship them so hard!"
},
{
"comment": "This episode is a rollercoaster ride. It had me gasping for breath!"
},
{
"comment": "The cinematography in this episode is stunning. Each frame is like a painting!"
},
{
"comment": "This episode is a game-changer. It took the series to a whole new level!"
},
{
"comment": "The performances in this episode are outstanding. The actors brought so much depth!"
},
{
"comment": "This episode had me at the edge of my seat. I couldn't look away!"
},
{
"comment": "The writing in this episode is exceptional. It's smart and thought-provoking!"
},
{
"comment": "This episode had me in tears. It's an emotional rollercoaster!"
},
{
"comment": "The tension in this episode is palpable. It kept me on the edge of my seat!"
},
{
"comment": "Just watched this episode and I'm in awe. It's a game-changer!"
},
{
"comment": "The cinematography in this episode is breathtaking. Each shot is visually stunning!"
},
{
"comment": "This episode had me hooked from the start. Such a compelling story!"
},
{
"comment": "The character development in this episode is remarkable. I love seeing their growth!"
},
{
"comment": "This episode is a rollercoaster of emotions. It made me laugh and cry!"
},
{
"comment": "The plot twists in this episode blew my mind. I didn't see them coming!"
},
{
"comment": "This episode left me speechless. It's a turning point in the series!"
},
{
"comment": "The acting in this episode is outstanding. The performances are so powerful!"
},
{
"comment": "This episode had me on the edge of my seat. It's so suspenseful!"
},
{
"comment": "The storytelling in this episode is brilliant. It kept me engaged throughout!"
},
{
"comment": "This episode touched my heart. It's emotional and impactful!"
},
{
"comment": "The chemistry between the characters in this episode is electric. Their interactions are captivating!"
},
{
"comment": "This episode is a rollercoaster ride. It had me on an emotional high!"
},
{
"comment": "The cinematography in this episode is stunning. Each shot is visually breathtaking!"
},
{
"comment": "This episode is a game-changer. It took the series to a whole new level!"
},
{
"comment": "The performances in this episode are outstanding. The actors delivered powerful portrayals!"
},
{
"comment": "This episode had me at the edge of my seat. I couldn't look away!"
},
{
"comment": "The writing in this episode is exceptional. It's gripping and thought-provoking!"
},
{
"comment": "This episode had me in tears. It's an emotional rollercoaster!"
},
{
"comment": "The tension in this episode is palpable. It kept me on the edge of my seat!"
},
{
"comment": "Just watched this episode and my mind is blown. It's a game-changer!"
},
{
"comment": "The suspense in this episode is unreal. Kept me on the edge of my seat!"
},
{
"comment": "This episode had me laughing out loud. So hilarious!"
},
{
"comment": "The emotional depth in this episode is incredible. It hit me right in the feels!"
},
{
"comment": "Just finished watching this episode and I'm in awe. So well-crafted!"
},
{
"comment": "The acting in this episode is phenomenal. Each performance is top-notch!"
},
{
"comment": "This episode had me hooked from start to finish. So captivating!"
},
{
"comment": "The plot development in this episode is mind-blowing. It's building up to something big!"
},
{
"comment": "Just witnessed the biggest cliffhanger in this episode. I need to know what happens next!"
},
{
"comment": "The chemistry between the characters in this episode is electric. I love their interactions!"
},
{
"comment": "This episode had me on an emotional rollercoaster. I laughed, I cried, I felt it all!"
},
{
"comment": "The cinematography in this episode is stunning. Each shot is visually breathtaking!"
},
{
"comment": "This episode is a rollercoaster ride of emotions. It's intense and heart-wrenching!"
},
{
"comment": "The dialogue in this episode is sharp and impactful. It kept me engaged!"
},
{
"comment": "This episode is a masterpiece. The writing, the acting, everything is on point!"
},
{
"comment": "The character growth in this episode is remarkable. They're evolving so beautifully!"
},
{
"comment": "This episode left me with so many questions. I can't wait for the next one!"
},
{
"comment": "The chemistry between the actors in this episode is undeniable. They have great synergy!"
},
{
"comment": "This episode is a rollercoaster of emotions. It made me laugh and cry!"
},
{
"comment": "The plot twists in this episode are mind-blowing. They kept me on my toes!"
},
{
"comment": "This episode left me speechless. It's a game-changer!"
},
{
"comment": "The acting in this episode is exceptional. The performances are so powerful!"
},
{
"comment": "This episode had me at the edge of my seat. So intense and suspenseful!"
},
{
"comment": "The storytelling in this episode is masterful. It's a captivating journey!"
},
{
"comment": "This episode had me in tears. It touched my heart in ways I didn't expect!"
},
{
"comment": "The chemistry between the characters in this episode is fire. I ship them so hard!"
},
{
"comment": "This episode is a rollercoaster ride. It had me gasping for breath!"
},
{
"comment": "The cinematography in this episode is stunning. Each frame is like a painting!"
},
{
"comment": "This episode is a game-changer. It took the series to a whole new level!"
},
{
"comment": "The performances in this episode are outstanding. The actors brought so much depth!"
},
{
"comment": "This episode had me on the edge of my seat. I couldn't look away!"
},
{
"comment": "The writing in this episode is exceptional. It's gripping and thought-provoking!"
},
{
"comment": "This episode had me in tears. It's an emotional rollercoaster!"
},
{
"comment": "The tension in this episode is palpable. It kept me on the edge of my seat!"
},
{
"comment": "Just watched this episode and I'm in awe. It's a game-changer!"
},
{
"comment": "The cinematography in this episode is breathtaking. Each shot is visually stunning!"
},
{
"comment": "This episode had me hooked from the start. Such a compelling story!"
},
{
"comment": "The character development in this episode is remarkable. I love seeing their growth!"
},
{
"comment": "This episode is a rollercoaster of emotions. It made me laugh and cry!"
},
{
"comment": "The plot twists in this episode blew my mind. I didn't see them coming!"
},
{
"comment": "This episode left me speechless. It's a turning point in the series!"
},
{
"comment": "The acting in this episode is outstanding. The performances are so powerful!"
},
{
"comment": "This episode had me on the edge of my seat. It's so suspenseful!"
},
{
"comment": "The storytelling in this episode is brilliant. It kept me engaged throughout!"
},
{
"comment": "This episode touched my heart. It's emotional and impactful!"
},
{
"comment": "The chemistry between the characters in this episode is electric. Their interactions are captivating!"
},
{
"comment": "This episode is a rollercoaster ride. It had me on an emotional high!"
},
{
"comment": "The cinematography in this episode is stunning. Each shot is visually breathtaking!"
},
{
"comment": "This episode is a game-changer. It took the series to a whole new level!"
},
{
"comment": "The performances in this episode are outstanding. The actors delivered powerful portrayals!"
},
{
"comment": "This episode had me at the edge of my seat. I couldn't look away!"
},
{
"comment": "The writing in this episode is exceptional. It's gripping and thought-provoking!"
},
{
"comment": "This episode had me in tears. It's an emotional rollercoaster!"
},
{
"comment": "The tension in this episode is palpable. It kept me on the edge of my seat!"
}
]

View File

@@ -0,0 +1,356 @@
[
{
"comment": "OMG, this movie is mind-blowing! Couldn't take my eyes off the screen!"
},
{
"comment": "Just watched this film and it's a total rollercoaster ride. So intense!"
},
{
"comment": "The visuals in this movie are insane. Such stunning cinematography!"
},
{
"comment": "Lead actor in this one is straight-up killing it. Their performance was on point!"
},
{
"comment": "This movie had me on the edge of my seat the whole time. Non-stop excitement!"
},
{
"comment": "The cast in this film is awesome. They brought so much energy to the story!"
},
{
"comment": "Laughed my head off watching this comedy flick. It's hilarious from start to finish!"
},
{
"comment": "Action scenes in this movie are epic. Heart-pounding and adrenaline-fueled!"
},
{
"comment": "This film had me in tears one moment and then cheering like crazy. So many emotions!"
},
{
"comment": "The soundtrack in this movie is fire. Can't get enough of those awesome tunes!"
},
{
"comment": "Just saw this movie and it blew my mind! So much suspense and unexpected twists."
},
{
"comment": "The setting in this film is breathtaking. It transports you to another world."
},
{
"comment": "The chemistry between the actors in this movie is off the charts. They were perfect together!"
},
{
"comment": "This movie had me hooked from the very beginning. Such an engaging storyline!"
},
{
"comment": "The humor in this film is on point. I was laughing throughout!"
},
{
"comment": "The special effects in this movie are mind-blowing. So realistic and visually stunning!"
},
{
"comment": "The performances in this film are outstanding. The actors truly brought their characters to life."
},
{
"comment": "I can't get over the incredible plot twists in this movie. Kept me guessing till the end!"
},
{
"comment": "This movie touched my heart. I felt all the emotions deeply."
},
{
"comment": "The action sequences in this film are like nothing I've seen before. Jaw-dropping!"
},
{
"comment": "Just watched this movie and it exceeded all my expectations. Highly recommended!"
},
{
"comment": "The cinematography in this film is stunning. Every frame is a work of art."
},
{
"comment": "The performances in this movie blew me away. So raw and powerful!"
},
{
"comment": "This movie is a rollercoaster of emotions. Laughter, tears, and everything in between!"
},
{
"comment": "The chemistry between the lead actors is off the charts. Their on-screen presence is electrifying!"
},
{
"comment": "Couldn't stop talking about this movie after watching it. It left a lasting impression!"
},
{
"comment": "The dialogue in this film is so witty and clever. I was quoting lines for days!"
},
{
"comment": "This movie had me at the edge of my seat throughout. Suspenseful and thrilling!"
},
{
"comment": "The soundtrack in this movie is pure perfection. It sets the mood so well!"
},
{
"comment": "Just watched this movie and I'm in awe. It's a masterpiece in every aspect!"
},
{
"comment": "The storytelling in this film is top-notch. It captivated me from start to finish."
},
{
"comment": "The performances in this movie are Oscar-worthy. So much talent on screen!"
},
{
"comment": "This movie is a total crowd-pleaser. Everyone should watch it!"
},
{
"comment": "The suspense in this film had me on the edge of my seat. Nerve-wracking and intense!"
},
{
"comment": "The cinematography in this movie is breathtaking. Each shot is visually stunning!"
},
{
"comment": "The characters in this film are so relatable. I felt like I knew them personally."
},
{
"comment": "This movie kept me guessing till the very end. So many unexpected twists!"
},
{
"comment": "The performances in this film are exceptional. I was completely immersed in the story."
},
{
"comment": "Just watched this movie and I'm speechless. It's a cinematic masterpiece!"
},
{
"comment": "The humor in this film had me laughing out loud. Pure comedic genius!"
},
{
"comment": "This movie is a visual feast. The production design is incredible!"
},
{
"comment": "The chemistry between the lead actors is undeniable. Their interactions are electric!"
},
{
"comment": "Couldn't get enough of this movie. I wish it never ended!"
},
{
"comment": "The cinematography in this film is stunning. It captured the essence of the story perfectly."
},
{
"comment": "The performances in this movie are outstanding. Each actor brought something unique to their role."
},
{
"comment": "This movie took me on an emotional rollercoaster. I laughed, I cried, and everything in between!"
},
{
"comment": "The action sequences in this film are jaw-dropping. It's like an adrenaline rush!"
},
{
"comment": "Just finished watching this movie and I'm blown away. It's a must-see!"
},
{
"comment": "The visuals in this film are stunning. I was in awe of the breathtaking cinematography!"
},
{
"comment": "The performances in this movie are phenomenal. The actors gave it their all!"
},
{
"comment": "This movie had me hooked from the beginning till the end. Such a gripping storyline!"
},
{
"comment": "The humor in this film is spot on. I couldn't stop laughing!"
},
{
"comment": "The special effects in this movie are mind-blowing. So realistic and visually stunning!"
},
{
"comment": "The cast in this movie is fantastic. Each actor brought depth and charisma to their character."
},
{
"comment": "This movie kept me on the edge of my seat. So many unexpected twists and turns!"
},
{
"comment": "The emotions in this film hit me hard. I laughed, I cried, and I felt everything in between."
},
{
"comment": "The action sequences in this movie are absolutely thrilling. It's a wild ride from start to finish!"
},
{
"comment": "Just watched this movie and it blew my mind! The story, the performances, everything was incredible."
},
{
"comment": "The cinematography in this film is breathtaking. Every shot is like a work of art!"
},
{
"comment": "The performances in this movie are outstanding. The actors brought so much depth to their characters!"
},
{
"comment": "This movie had me completely engrossed. I couldn't take my eyes off the screen!"
},
{
"comment": "The humor in this film is on point. I couldn't stop laughing!"
},
{
"comment": "The special effects in this movie are mind-blowing. They added an extra layer of awesomeness!"
},
{
"comment": "The cast in this movie is stellar. Each actor brought their A-game!"
},
{
"comment": "This movie had me guessing till the end. So many plot twists and surprises!"
},
{
"comment": "The performances in this film are outstanding. Each actor brought their character to life in a remarkable way."
},
{
"comment": "Just watched this movie and I'm in awe. It's a cinematic masterpiece!"
},
{
"comment": "The comedy in this film had me laughing so hard. It's pure comedic gold!"
},
{
"comment": "This movie is visually stunning. The cinematography is breathtaking!"
},
{
"comment": "The chemistry between the actors in this film is electric. They had amazing on-screen dynamics!"
},
{
"comment": "Couldn't stop thinking about this movie after watching it. It left a lasting impression!"
},
{
"comment": "The dialogue in this film is so witty and clever. It had me laughing out loud!"
},
{
"comment": "This movie had me on the edge of my seat the whole time. Gripping and suspenseful!"
},
{
"comment": "The soundtrack in this movie is perfect. It sets the mood and enhances every scene!"
},
{
"comment": "Just watched this movie and I'm blown away. It's a must-see!"
},
{
"comment": "The storytelling in this film is captivating. It kept me engaged from start to finish."
},
{
"comment": "The performances in this movie are exceptional. Each actor delivered a powerful portrayal!"
},
{
"comment": "This movie is a crowd-pleaser. It has something for everyone!"
},
{
"comment": "The suspense in this film had me on the edge of my seat. It kept me guessing till the end!"
},
{
"comment": "The cinematography in this movie is breathtaking. Each shot is visually stunning!"
},
{
"comment": "The characters in this film are so relatable. I felt like I knew them personally."
},
{
"comment": "This movie kept me on the edge of my seat. So many unexpected twists and turns!"
},
{
"comment": "The performances in this film are exceptional. I was completely immersed in the story."
},
{
"comment": "Just watched this movie and I'm speechless. It's a cinematic masterpiece!"
},
{
"comment": "The humor in this film had me laughing out loud. Pure comedic genius!"
},
{
"comment": "This movie is a visual feast. The production design is incredible!"
},
{
"comment": "The chemistry between the lead actors is undeniable. Their on-screen interactions are electric!"
},
{
"comment": "Couldn't get enough of this movie. I wish it never ended!"
},
{
"comment": "The cinematography in this film is stunning. It captured the essence of the story perfectly."
},
{
"comment": "The performances in this movie are outstanding. Each actor brought something unique to their role."
},
{
"comment": "This movie took me on an emotional rollercoaster. I laughed, I cried, and everything in between!"
},
{
"comment": "The action sequences in this film are jaw-dropping. It's like an adrenaline rush!"
},
{
"comment": "Just finished watching this movie and I'm blown away. It's a must-see!"
},
{
"comment": "The visuals in this film are stunning. I was in awe of the breathtaking cinematography!"
},
{
"comment": "The performances in this movie are phenomenal. The actors gave it their all!"
},
{
"comment": "This movie had me hooked from the beginning till the end. Such a gripping storyline!"
},
{
"comment": "The humor in this film is spot on. I couldn't stop laughing!"
},
{
"comment": "The special effects in this movie are mind-blowing. So realistic and visually stunning!"
},
{
"comment": "The cast in this movie is fantastic. Each actor brought depth and charisma to their character."
},
{
"comment": "This movie kept me guessing till the very end. So many unexpected twists!"
},
{
"comment": "The performances in this film are exceptional. I was completely immersed in the story."
},
{
"comment": "Just watched this movie and I'm in awe. It's a cinematic masterpiece!"
},
{
"comment": "The cinematography in this film is breathtaking. Every shot is like a work of art!"
},
{
"comment": "The performances in this movie are outstanding. Each actor brought so much depth to their characters!"
},
{
"comment": "This movie had me completely engrossed. I couldn't take my eyes off the screen!"
},
{
"comment": "The humor in this film is on point. I couldn't stop laughing!"
},
{
"comment": "The special effects in this movie are mind-blowing. They added an extra layer of awesomeness!"
},
{
"comment": "The cast in this movie is stellar. Each actor brought their A-game!"
},
{
"comment": "This movie had me guessing till the end. So many plot twists and surprises!"
},
{
"comment": "The performances in this film are outstanding. Each actor brought their character to life in a remarkable way."
},
{
"comment": "Just watched this movie and I'm in awe. It's a cinematic masterpiece!"
},
{
"comment": "The comedy in this film had me laughing so hard. It's pure comedic gold!"
},
{
"comment": "This movie is visually stunning. The cinematography is breathtaking!"
},
{
"comment": "The chemistry between the actors in this film is electric. They had amazing on-screen dynamics!"
},
{
"comment": "Couldn't stop thinking about this movie after watching it. It left a lasting impression!"
},
{
"comment": "The dialogue in this film is so witty and clever. It had me laughing out loud!"
},
{
"comment": "This movie had me on the edge of my seat the whole time. Gripping and suspenseful!"
},
{
"comment": "The soundtrack in this movie is perfect. It sets the mood and enhances every scene!"
}
]

View File

@@ -0,0 +1,523 @@
[
{
"title": "A Must-Watch!",
"body": "OMG, this movie is a total masterpiece! The storytelling, acting, and visuals are on another level. You gotta see it!",
"score": 9
},
{
"title": "So Thrilling and Action-Packed!",
"body": "Hold onto your seats, folks! This movie is an adrenaline rush from start to finish. The action scenes will blow your mind!",
"score": 8
},
{
"title": "Emotionally Powerful",
"body": "Get ready to feel all the feels! This movie dives deep into emotions and hits you right in the heart. It's a tearjerker, but in a good way!",
"score": 7
},
{
"title": "Laugh-Out-Loud Funny!",
"body": "If you're in need of a good laugh, this movie is the perfect remedy! The jokes are hilarious, and the humor is off the charts!",
"score": 8
},
{
"title": "Visually Jaw-Dropping",
"body": "Prepare to have your mind blown by the stunning visuals in this movie! The cinematography and special effects are out of this world!",
"score": 9
},
{
"title": "Keeps You Guessing!",
"body": "Whoa, this movie is a wild ride! It's full of suspense, unexpected twists, and turns that will leave you guessing until the very end!",
"score": 8
},
{
"title": "Heartwarming and Inspiring",
"body": "This movie will warm your heart and give you all the feels! It's inspiring, uplifting, and leaves you with a big smile on your face!",
"score": 9
},
{
"title": "Gets You Thinking",
"body": "Get ready to have your mind blown! This movie tackles deep themes that make you question everything. It'll keep you pondering for days!",
"score": 7
},
{
"title": "A Thrill Ride to Remember",
"body": "Buckle up, folks! This movie is a non-stop thrill ride that will have you at the edge of your seat, biting your nails, and craving for more!",
"score": 9
},
{
"title": "Mind-Blowing Performances",
"body": "The cast in this movie is absolutely incredible! They deliver mind-blowing performances that will leave you in awe. Kudos to the actors!",
"score": 8
},
{
"title": "Disappointing and Underwhelming",
"body": "I had high hopes for this movie, but it fell short. The plot was predictable, and the performances felt lackluster. Not worth the hype.",
"score": 4
},
{
"title": "Lacks Originality",
"body": "Unfortunately, this movie felt like a rehash of familiar tropes. The story offered nothing new, and I found myself bored and unengaged.",
"score": 3
},
{
"title": "A Missed Opportunity",
"body": "With such potential, this movie failed to deliver. The pacing was off, and the characters lacked depth. It's a forgettable experience.",
"score": 5
},
{
"title": "Impressive Cinematic Experience",
"body": "This movie is a visual feast for the eyes! The stunning cinematography and breathtaking set pieces create an immersive cinematic experience. Highly recommended!",
"score": 9
},
{
"title": "Captivating Storyline",
"body": "From beginning to end, this movie had me hooked with its compelling storyline. The twists and turns kept me engaged, eagerly waiting to see what happens next!",
"score": 8
},
{
"title": "Powerful and Thought-Provoking",
"body": "This movie tackles important themes and leaves a lasting impact. It makes you reflect on society and the human condition. Prepare for a thought-provoking journey!",
"score": 7
},
{
"title": "Hilarious and Heartwarming",
"body": "Laughter and warm fuzzies guaranteed! This movie strikes the perfect balance between comedy and heart, delivering memorable moments that will leave you smiling.",
"score": 8
},
{
"title": "Visually Stunning Masterpiece",
"body": "This movie is a visual marvel. The attention to detail, breathtaking visuals, and stunning cinematography create a feast for the eyes. A true cinematic masterpiece!",
"score": 9
},
{
"title": "Intriguing and Suspenseful",
"body": "Prepare for a nail-biting experience! This movie keeps you on the edge of your seat with its gripping plot and suspenseful sequences. It will leave you guessing until the end!",
"score": 8
},
{
"title": "Inspiring and Uplifting",
"body": "This movie is a feel-good gem that leaves you with a renewed sense of inspiration and hope. It touches your heart and reminds you of the power of dreams!",
"score": 9
},
{
"title": "Mind-Expanding Story",
"body": "This movie expands your mind and challenges your perspective. It delves into deep concepts and takes you on a journey of self-discovery. Prepare to be amazed!",
"score": 7
},
{
"title": "Action-Packed Thrills",
"body": "Hold onto your popcorn! This movie delivers non-stop action and thrilling sequences that will leave you breathless. Get ready for an adrenaline-fueled joyride!",
"score": 9
},
{
"title": "Incredible Performances",
"body": "The performances in this movie are top-notch! The talented cast brings their characters to life, delivering powerful and nuanced portrayals. A true acting masterclass!",
"score": 8
},
{
"title": "Disappointing Execution",
"body": "Despite promising elements, this movie fails to live up to expectations. The execution falls flat, leaving a sense of missed opportunities and unfulfilled potential.",
"score": 4
},
{
"title": "Lackluster Storytelling",
"body": "The story in this movie lacks depth and fails to engage. It feels clichéd and fails to offer any surprises or compelling narrative arcs. Disappointing overall.",
"score": 3
},
{
"title": "Underdeveloped Characters",
"body": "The characters in this movie lack depth and fail to leave a lasting impression. Their motivations and relationships feel superficial, leaving much to be desired.",
"score": 5
},
{
"title": "Visually Mediocre",
"body": "While the story may have potential, the visual execution leaves much to be desired. The cinematography and special effects fail to impress, resulting in a lackluster experience.",
"score": 4
},
{
"title": "Predictable and Formulaic",
"body": "This movie follows a predictable formula, leaving little room for surprises. The plot unfolds exactly as expected, offering little excitement or suspense.",
"score": 3
},
{
"title": "A Forgettable Experience",
"body": "Despite its promising premise, this movie fails to make a lasting impact. It lacks memorable moments and fails to leave a lasting impression. Easily forgettable.",
"score": 5
},
{
"title": "Engaging and Enthralling",
"body": "This movie grabs your attention from the very beginning and never lets go. The engaging storyline and captivating performances make it a must-watch!",
"score": 8
},
{
"title": "Deeply Moving and Emotional",
"body": "Be prepared for an emotional rollercoaster! This movie touches your heart and evokes a range of emotions. It's a powerful and moving cinematic experience!",
"score": 9
},
{
"title": "An Entertaining Delight",
"body": "This movie is pure entertainment! It's a delightful escape from reality, filled with fun and excitement. Sit back, relax, and enjoy the ride!",
"score": 8
},
{
"title": "Visually Mesmerizing",
"body": "Prepare to be mesmerized by the stunning visuals in this movie. The breathtaking imagery and artistic cinematography create a feast for the eyes!",
"score": 9
},
{
"title": "Suspenseful and Thrilling",
"body": "This movie keeps you on the edge of your seat with its suspenseful plot and heart-pounding moments. It's a gripping thrill ride you won't forget!",
"score": 8
},
{
"title": "An Inspiring Journey",
"body": "Get ready to be inspired! This movie takes you on a transformative journey, filled with life lessons, hope, and a renewed sense of purpose.",
"score": 9
},
{
"title": "Intellectually Stimulating",
"body": "This movie challenges your intellect and provokes thought. It raises important questions and offers unique perspectives. Prepare for a mentally engaging experience!",
"score": 7
},
{
"title": "Action-Packed and Exciting",
"body": "This movie is a thrilling adventure from start to finish. The action sequences are mind-blowing, and the excitement never lets up. Pure adrenaline rush!",
"score": 9
},
{
"title": "Outstanding Performances",
"body": "The actors in this movie deliver exceptional performances, bringing their characters to life with depth and authenticity. Truly remarkable!",
"score": 8
},
{
"title": "Disappointing Plot",
"body": "Unfortunately, the plot of this movie fails to captivate. It lacks originality and fails to offer any compelling twists or turns. Disappointing overall.",
"score": 4
},
{
"title": "Lacks Substance",
"body": "This movie may have flashy visuals, but it lacks substance. The story feels shallow and fails to resonate on a deeper level. A forgettable experience.",
"score": 3
},
{
"title": "Underwhelming Characters",
"body": "The characters in this movie are one-dimensional and fail to leave a lasting impression. Their development is lacking, resulting in a lackluster experience.",
"score": 5
},
{
"title": "Visually Unremarkable",
"body": "The visual presentation in this movie is unremarkable. It lacks creativity and fails to leave a lasting visual impact. A missed opportunity.",
"score": 4
},
{
"title": "Formulaic and Predictable",
"body": "This movie follows a familiar formula, leaving little room for surprises. The plot unfolds exactly as expected, resulting in a lack of excitement or intrigue.",
"score": 3
},
{
"title": "A Forgettable Journey",
"body": "Despite its initial promise, this movie fails to leave a lasting impact. It lacks memorable moments and fails to make a lasting impression. Easily forgettable.",
"score": 5
},
{
"title": "Compelling and Engrossing",
"body": "This movie hooks you from the beginning and keeps you engaged throughout. The captivating story and stellar performances make it a must-watch!",
"score": 8
},
{
"title": "A Rollercoaster of Emotions",
"body": "Get ready to laugh, cry, and everything in between! This movie tugs at your heartstrings and takes you on an emotional rollercoaster. Prepare for a truly moving experience!",
"score": 9
},
{
"title": "Pure Fun and Entertainment",
"body": "This movie is pure entertainment! It's a fun-filled adventure that will keep you entertained from start to finish. Sit back, relax, and enjoy the show!",
"score": 8
},
{
"title": "A Visual Masterpiece",
"body": "Prepare to be visually amazed! This movie boasts breathtaking visuals, stunning cinematography, and a visual style that will leave you in awe.",
"score": 9
},
{
"title": "Thrilling and Suspenseful",
"body": "This movie will keep you on the edge of your seat with its thrilling plot and suspenseful twists. It's a gripping cinematic experience you won't want to miss!",
"score": 8
},
{
"title": "An Inspiring and Motivational Tale",
"body": "This movie inspires you to chase your dreams and overcome obstacles. It's a motivational tale that leaves you feeling empowered and ready to take on the world!",
"score": 9
},
{
"title": "Intellectually Engaging",
"body": "Prepare to have your mind stimulated! This movie challenges your intellect with its thought-provoking concepts and complex narrative. A must-watch for thinkers!",
"score": 7
},
{
"title": "Action-Packed and Thrilling",
"body": "Hold onto your seats! This movie delivers high-octane action and thrilling sequences that will keep you on the edge of your seat. A pulse-pounding experience!",
"score": 9
},
{
"title": "Exceptional Acting",
"body": "The performances in this movie are truly remarkable. The cast delivers nuanced and powerful performances that elevate the storytelling. Bravo!",
"score": 8
},
{
"title": "Disappointing Execution",
"body": "Despite its promising premise, this movie fails to deliver. The execution falls flat, leaving a sense of missed opportunities and unfulfilled potential.",
"score": 4
},
{
"title": "Lackluster Storytelling",
"body": "The storytelling in this movie lacks depth and fails to engage. It feels clichéd and fails to offer any surprises or compelling narrative arcs. Disappointing overall.",
"score": 3
},
{
"title": "Underdeveloped Characters",
"body": "The characters in this movie lack depth and fail to leave a lasting impression. Their motivations and relationships feel superficial, leaving much to be desired.",
"score": 5
},
{
"title": "Visually Mediocre",
"body": "While the story may have potential, the visual execution leaves much to be desired. The cinematography and special effects fail to impress, resulting in a lackluster experience.",
"score": 4
},
{
"title": "Predictable and Formulaic",
"body": "This movie follows a predictable formula, leaving little room for surprises. The plot unfolds exactly as expected, offering little excitement or suspense.",
"score": 3
},
{
"title": "A Forgettable Experience",
"body": "Despite its promising premise, this movie fails to make a lasting impact. It lacks memorable moments and fails to leave a lasting impression. Easily forgettable.",
"score": 5
},
{
"title": "Engaging and Enthralling",
"body": "This movie grabs your attention from the very beginning and never lets go. The engaging storyline and captivating performances make it a must-watch!",
"score": 8
},
{
"title": "Deeply Moving and Emotional",
"body": "Be prepared for an emotional rollercoaster! This movie touches your heart and evokes a range of emotions. It's a powerful and moving cinematic experience!",
"score": 9
},
{
"title": "An Entertaining Delight",
"body": "This movie is pure entertainment! It's a delightful escape from reality, filled with fun and excitement. Sit back, relax, and enjoy the ride!",
"score": 8
},
{
"title": "Visually Mesmerizing",
"body": "Prepare to be mesmerized by the stunning visuals in this movie. The breathtaking imagery and artistic cinematography create a feast for the eyes!",
"score": 9
},
{
"title": "Suspenseful and Thrilling",
"body": "This movie keeps you on the edge of your seat with its suspenseful plot and heart-pounding moments. It's a gripping thrill ride you won't forget!",
"score": 8
},
{
"title": "An Inspiring Journey",
"body": "Get ready to be inspired! This movie takes you on a transformative journey, filled with life lessons, hope, and a renewed sense of purpose.",
"score": 9
},
{
"title": "Intellectually Stimulating",
"body": "This movie challenges your intellect and provokes thought. It raises important questions and offers unique perspectives. Prepare for a mentally engaging experience!",
"score": 7
},
{
"title": "Action-Packed and Exciting",
"body": "This movie is a thrilling adventure from start to finish. The action sequences are mind-blowing, and the excitement never lets up. Pure adrenaline rush!",
"score": 9
},
{
"title": "Outstanding Performances",
"body": "The actors in this movie deliver exceptional performances, bringing their characters to life with depth and authenticity. Truly remarkable!",
"score": 8
},
{
"title": "Disappointing Plot",
"body": "Unfortunately, the plot of this movie fails to captivate. It lacks originality and fails to offer any compelling twists or turns. Disappointing overall.",
"score": 4
},
{
"title": "Lacks Substance",
"body": "This movie may have flashy visuals, but it lacks substance. The story feels shallow and fails to resonate on a deeper level. A forgettable experience.",
"score": 3
},
{
"title": "Underwhelming Characters",
"body": "The characters in this movie are one-dimensional and fail to leave a lasting impression. Their development is lacking, resulting in a lackluster experience.",
"score": 5
},
{
"title": "Visually Unremarkable",
"body": "The visual presentation in this movie is unremarkable. It lacks creativity and fails to leave a lasting visual impact. A missed opportunity.",
"score": 4
},
{
"title": "Formulaic and Predictable",
"body": "This movie follows a familiar formula, leaving little room for surprises. The plot unfolds exactly as expected, resulting in a lack of excitement or intrigue.",
"score": 3
},
{
"title": "A Forgettable Journey",
"body": "Despite its initial promise, this movie fails to leave a lasting impact. It lacks memorable moments and fails to make a lasting impression. Easily forgettable.",
"score": 5
},
{
"title": "Compelling and Engrossing",
"body": "This movie hooks you from the beginning and keeps you engaged throughout. The captivating story and stellar performances make it a must-watch!",
"score": 8
},
{
"title": "A Rollercoaster of Emotions",
"body": "Get ready to laugh, cry, and everything in between! This movie tugs at your heartstrings and takes you on an emotional rollercoaster. Prepare for a truly moving experience!",
"score": 9
},
{
"title": "Pure Fun and Entertainment",
"body": "This movie is pure entertainment! It's a fun-filled adventure that will keep you entertained from start to finish. Sit back, relax, and enjoy the show!",
"score": 8
},
{
"title": "A Visual Masterpiece",
"body": "Prepare to be visually amazed! This movie boasts breathtaking visuals, stunning cinematography, and a visual style that will leave you in awe.",
"score": 9
},
{
"title": "Thrilling and Suspenseful",
"body": "This movie will keep you on the edge of your seat with its thrilling plot and suspenseful twists. It's a gripping cinematic experience you won't want to miss!",
"score": 8
},
{
"title": "An Inspiring and Motivational Tale",
"body": "This movie inspires you to chase your dreams and overcome obstacles. It's a motivational tale that leaves you feeling empowered and ready to take on the world!",
"score": 9
},
{
"title": "Intellectually Engaging",
"body": "Prepare to have your mind stimulated! This movie challenges your intellect with its thought-provoking concepts and complex narrative. A must-watch for thinkers!",
"score": 7
},
{
"title": "Action-Packed and Thrilling",
"body": "Hold onto your seats! This movie delivers high-octane action and thrilling sequences that will keep you on the edge of your seat. A pulse-pounding experience!",
"score": 9
},
{
"title": "Exceptional Acting",
"body": "The performances in this movie are truly remarkable. The cast delivers nuanced and powerful performances that elevate the storytelling. Bravo!",
"score": 8
},
{
"title": "Disappointing Execution",
"body": "Despite its promising premise, this movie fails to deliver. The execution falls flat, leaving a sense of missed opportunities and unfulfilled potential.",
"score": 4
},
{
"title": "Lackluster Storytelling",
"body": "The storytelling in this movie lacks depth and fails to engage. It feels clichéd and fails to offer any surprises or compelling narrative arcs. Disappointing overall.",
"score": 3
},
{
"title": "Underdeveloped Characters",
"body": "The characters in this movie lack depth and fail to leave a lasting impression. Their motivations and relationships feel superficial, leaving much to be desired.",
"score": 5
},
{
"title": "Visually Mediocre",
"body": "While the story may have potential, the visual execution leaves much to be desired. The cinematography and special effects fail to impress, resulting in a lackluster experience.",
"score": 4
},
{
"title": "Predictable and Formulaic",
"body": "This movie follows a predictable formula, leaving little room for surprises. The plot unfolds exactly as expected, offering little excitement or suspense.",
"score": 3
},
{
"title": "A Forgettable Experience",
"body": "Despite its promising premise, this movie fails to make a lasting impact. It lacks memorable moments and fails to leave a lasting impression. Easily forgettable.",
"score": 5
},
{
"title": "Engaging and Enthralling",
"body": "This movie grabs your attention from the very beginning and never lets go. The engaging storyline and captivating performances make it a must-watch!",
"score": 8
},
{
"title": "Deeply Moving and Emotional",
"body": "Be prepared for an emotional rollercoaster! This movie touches your heart and evokes a range of emotions. It's a powerful and moving cinematic experience!",
"score": 9
},
{
"title": "An Entertaining Delight",
"body": "This movie is pure entertainment! It's a delightful escape from reality, filled with fun and excitement. Sit back, relax, and enjoy the ride!",
"score": 8
},
{
"title": "Visually Mesmerizing",
"body": "Prepare to be mesmerized by the stunning visuals in this movie. The breathtaking imagery and artistic cinematography create a feast for the eyes!",
"score": 9
},
{
"title": "Suspenseful and Thrilling",
"body": "This movie keeps you on the edge of your seat with its suspenseful plot and heart-pounding moments. It's a gripping thrill ride you won't forget!",
"score": 8
},
{
"title": "An Inspiring Journey",
"body": "Get ready to be inspired! This movie takes you on a transformative journey, filled with life lessons, hope, and a renewed sense of purpose.",
"score": 9
},
{
"title": "Intellectually Stimulating",
"body": "This movie challenges your intellect and provokes thought. It raises important questions and offers unique perspectives. Prepare for a mentally engaging experience!",
"score": 7
},
{
"title": "Action-Packed and Exciting",
"body": "This movie is a thrilling adventure from start to finish. The action sequences are mind-blowing, and the excitement never lets up. Pure adrenaline rush!",
"score": 9
},
{
"title": "Outstanding Performances",
"body": "The actors in this movie deliver exceptional performances, bringing their characters to life with depth and authenticity. Truly remarkable!",
"score": 8
},
{
"title": "Disappointing Plot",
"body": "Unfortunately, the plot of this movie fails to captivate. It lacks originality and fails to offer any compelling twists or turns. Disappointing overall.",
"score": 4
},
{
"title": "Lacks Substance",
"body": "This movie may have flashy visuals, but it lacks substance. The story feels shallow and fails to resonate on a deeper level. A forgettable experience.",
"score": 3
},
{
"title": "Underwhelming Characters",
"body": "The characters in this movie are one-dimensional and fail to leave a lasting impression. Their development is lacking, resulting in a lackluster experience.",
"score": 5
},
{
"title": "Visually Unremarkable",
"body": "The visual presentation in this movie is unremarkable. It lacks creativity and fails to leave a lasting visual impact. A missed opportunity.",
"score": 4
},
{
"title": "Formulaic and Predictable",
"body": "This movie follows a familiar formula, leaving little room for surprises. The plot unfolds exactly as expected, resulting in a lack of excitement or intrigue.",
"score": 3
},
{
"title": "A Forgettable Journey",
"body": "Despite its initial promise, this movie fails to leave a lasting impact. It lacks memorable moments and fails to make a lasting impression. Easily forgettable.",
"score": 5
}
]

View File

@@ -0,0 +1,329 @@
[
{
"comment": "OMG, this series is mind-blowing! I can't stop watching!"
},
{
"comment": "Just binge-watched this series and I'm hooked. It's so addictive!"
},
{
"comment": "The storyline in this series is insane. So many twists and turns!"
},
{
"comment": "The cast in this series is amazing. Each actor brings something unique!"
},
{
"comment": "This series had me on the edge of my seat the whole time. So suspenseful!"
},
{
"comment": "The chemistry between the characters in this series is off the charts. Love it!"
},
{
"comment": "Laughed my heart out watching this comedy series. It's hilarious!"
},
{
"comment": "The drama in this series is so intense. It keeps me wanting more!"
},
{
"comment": "Just finished the latest season of this series. Mind officially blown!"
},
{
"comment": "The soundtrack in this series is fire. Can't get enough of it!"
},
{
"comment": "This series is binge-worthy. I can't stop clicking 'Next Episode'!"
},
{
"comment": "The character development in this series is amazing. I feel so connected to them!"
},
{
"comment": "This series has me emotionally invested. I laugh, I cry, I feel it all!"
},
{
"comment": "The cliffhangers in this series are killing me. I need to know what happens next!"
},
{
"comment": "The acting in this series is top-notch. Such talent on display!"
},
{
"comment": "This series keeps me guessing at every turn. Never a dull moment!"
},
{
"comment": "The relationships in this series are so compelling. I'm rooting for them!"
},
{
"comment": "This series is pure entertainment. It's my guilty pleasure!"
},
{
"comment": "The writing in this series is incredible. So many quotable lines!"
},
{
"comment": "This series has a unique concept. Refreshing and original!"
},
{
"comment": "The suspense in this series is killing me. Can't wait for the next episode!"
},
{
"comment": "The chemistry between the cast members is amazing. They're like a family!"
},
{
"comment": "This series has a great mix of genres. There's something for everyone!"
},
{
"comment": "The plot twists in this series are mind-blowing. I never saw them coming!"
},
{
"comment": "The performances in this series are outstanding. Each actor shines!"
},
{
"comment": "This series is addictive. I'm obsessed with the characters and their stories!"
},
{
"comment": "The humor in this series is on point. I can't stop laughing!"
},
{
"comment": "This series keeps me on the edge of my seat. So many unexpected surprises!"
},
{
"comment": "The soundtrack in this series is a vibe. It sets the perfect mood!"
},
{
"comment": "This series is a rollercoaster of emotions. I'm hooked!"
},
{
"comment": "The world-building in this series is impressive. So immersive!"
},
{
"comment": "This series has strong female characters. They're empowering and inspiring!"
},
{
"comment": "The cinematography in this series is stunning. Every shot is beautiful!"
},
{
"comment": "This series has incredible storytelling. I'm captivated!"
},
{
"comment": "The performances in this series are incredible. Each actor brings depth and nuance!"
},
{
"comment": "This series is an emotional rollercoaster. It tugs at my heartstrings!"
},
{
"comment": "The suspense in this series is intense. I can't stop watching!"
},
{
"comment": "This series has a diverse and inclusive cast. Representation matters!"
},
{
"comment": "The writing in this series is superb. It keeps me engaged!"
},
{
"comment": "This series is my latest obsession. I can't get enough of it!"
},
{
"comment": "The chemistry between the actors in this series is fire. They have amazing on-screen dynamics!"
},
{
"comment": "This series is a rollercoaster ride of emotions. It takes me on a journey!"
},
{
"comment": "The world-building in this series is phenomenal. It feels so real!"
},
{
"comment": "This series tackles important social issues. It's thought-provoking!"
},
{
"comment": "The cinematography in this series is stunning. Each frame is visually captivating!"
},
{
"comment": "This series has a strong ensemble cast. Each actor brings something special!"
},
{
"comment": "The plot twists in this series keep me on my toes. Never a dull moment!"
},
{
"comment": "This series has me emotionally invested. I feel like I'm part of their world!"
},
{
"comment": "The character dynamics in this series are so well-developed. I love their interactions!"
},
{
"comment": "This series has so many memorable moments. It leaves a lasting impression!"
},
{
"comment": "The performances in this series are exceptional. Each actor brings depth to their character!"
},
{
"comment": "This series is addictive. I can't stop watching episode after episode!"
},
{
"comment": "The humor in this series is hilarious. It never fails to make me laugh!"
},
{
"comment": "This series keeps me on the edge of my seat. It's a wild ride!"
},
{
"comment": "The soundtrack in this series is incredible. It enhances the storytelling!"
},
{
"comment": "This series is an emotional rollercoaster. It makes me laugh and cry!"
},
{
"comment": "The attention to detail in this series is impressive. It's a visual feast!"
},
{
"comment": "This series has a diverse cast with amazing performances. It's inclusive and representative!"
},
{
"comment": "The writing in this series is brilliant. The dialogue is sharp and engaging!"
},
{
"comment": "This series has become my new obsession. I can't get enough of it!"
},
{
"comment": "The chemistry between the actors in this series is electric. Their performances are captivating!"
},
{
"comment": "This series is a whirlwind of emotions. It has me invested in every character!"
},
{
"comment": "The world-building in this series is rich and immersive. It feels like a whole new universe!"
},
{
"comment": "This series tackles relevant and timely themes. It's thought-provoking and impactful!"
},
{
"comment": "The cinematography in this series is stunning. It adds another layer of beauty to the storytelling!"
},
{
"comment": "This series has an incredible ensemble cast. The chemistry between the actors is incredible!"
},
{
"comment": "The plot twists in this series are mind-bending. It keeps me guessing all the time!"
},
{
"comment": "This series has a strong emotional core. It touches my heart and leaves a lasting impact!"
},
{
"comment": "The character development in this series is exceptional. I love seeing their growth!"
},
{
"comment": "This series is so addictive. I can't stop watching. It's my latest obsession!"
},
{
"comment": "The humor in this series is top-notch. It's a perfect balance of comedy and heart!"
},
{
"comment": "This series is full of surprises. It keeps me guessing and on the edge of my seat!"
},
{
"comment": "The soundtrack in this series is phenomenal. It adds so much to the atmosphere!"
},
{
"comment": "This series is an emotional rollercoaster. It takes me through highs and lows!"
},
{
"comment": "The attention to detail in this series is incredible. The production value is top-notch!"
},
{
"comment": "This series has a diverse cast that brings authenticity and representation. It's amazing!"
},
{
"comment": "The writing in this series is exceptional. It keeps me engaged and invested in the story!"
},
{
"comment": "This series has become my new addiction. I can't get enough of it!"
},
{
"comment": "The chemistry between the actors in this series is off the charts. They have incredible synergy!"
},
{
"comment": "This series is an emotional rollercoaster. It makes me feel all the feels!"
},
{
"comment": "The world-building in this series is immersive and captivating. It feels like a whole new reality!"
},
{
"comment": "This series tackles important issues with sensitivity and depth. It's thought-provoking!"
},
{
"comment": "The cinematography in this series is stunning. It's visually breathtaking!"
},
{
"comment": "This series has an amazing ensemble cast. Each actor brings their A-game!"
},
{
"comment": "The plot twists in this series are mind-blowing. It keeps me on the edge of my seat!"
},
{
"comment": "This series has characters I can't help but root for. I'm emotionally invested!"
},
{
"comment": "The character dynamics in this series are so well-written. The relationships feel real!"
},
{
"comment": "This series has so many unforgettable moments. It leaves a lasting impact!"
},
{
"comment": "The performances in this series are outstanding. Each actor brings depth to their role!"
},
{
"comment": "Just watched the latest episode of this series and it's mind-blowing! Can't wait for more!"
},
{
"comment": "This series keeps getting better and better with each episode. I'm hooked!"
},
{
"comment": "The latest episode of this series left me speechless. It's a game-changer!"
},
{
"comment": "The character development in this series is outstanding. I love seeing them grow!"
},
{
"comment": "The latest episode of this series had me on the edge of my seat. So thrilling!"
},
{
"comment": "I just can't get enough of this series. Each episode leaves me wanting more!"
},
{
"comment": "The latest episode of this series had me laughing out loud. It's so funny!"
},
{
"comment": "This series knows how to keep me invested. The latest episode was so captivating!"
},
{
"comment": "The latest episode of this series had so many unexpected twists. It's unpredictable!"
},
{
"comment": "I'm completely obsessed with this series. The latest episode blew my mind!"
},
{
"comment": "The latest episode of this series is visually stunning. The production value is incredible!"
},
{
"comment": "This series just keeps raising the bar. The latest episode was exceptional!"
},
{
"comment": "The chemistry between the characters in this series is off the charts. I love their dynamics!"
},
{
"comment": "The latest episode of this series tugged at my heartstrings. It's so emotional!"
},
{
"comment": "I'm always left wanting more after each episode of this series. It's addictive!"
},
{
"comment": "The latest episode of this series had me at the edge of my seat. So intense!"
},
{
"comment": "This series continues to surprise me. The latest episode had an unexpected twist!"
},
{
"comment": "The latest episode of this series had me completely hooked. It's so well-written!"
},
{
"comment": "I just can't get enough of this series. The latest episode was phenomenal!"
},
{
"comment": "The latest episode of this series had me on an emotional rollercoaster. It's powerful!"
}
]

View File

@@ -0,0 +1,507 @@
[
{
"title": "Binge-Worthy Series!",
"body": "I couldn't stop watching this series! Each episode left me wanting more. The storyline and characters are captivating. Highly recommended!",
"score": 9
},
{
"title": "Addictive and Engaging",
"body": "This series had me hooked from the very first episode. The plot twists and character development kept me invested throughout. Can't wait for the next season!",
"score": 8
},
{
"title": "Emotional Rollercoaster",
"body": "This series took me on an emotional journey. I laughed, I cried, and I felt deeply connected to the characters. It's a must-watch!",
"score": 7
},
{
"title": "Hilarious and Heartwarming",
"body": "I couldn't stop laughing while watching this series. The humor is spot-on, and the heartwarming moments tug at your heartstrings. A perfect balance!",
"score": 8
},
{
"title": "Intriguing and Mysterious",
"body": "This series kept me guessing until the very end. The mystery and suspense had me on the edge of my seat. A thrilling ride!",
"score": 9
},
{
"title": "Well-Written and Gripping",
"body": "The writing in this series is exceptional. The dialogue is sharp, and the storylines are compelling. It's a masterclass in storytelling!",
"score": 8
},
{
"title": "Powerful and Thought-Provoking",
"body": "This series tackles important social issues and provokes thought. It sheds light on topics that need to be discussed. A truly impactful watch!",
"score": 7
},
{
"title": "Addictive Plot Twists",
"body": "Just when I thought I had it figured out, this series hit me with unexpected plot twists. It kept me engaged and eagerly anticipating the next episode!",
"score": 8
},
{
"title": "Relatable and Authentic",
"body": "The characters in this series feel like real people. I could relate to their struggles and victories. It's a genuine and authentic portrayal!",
"score": 9
},
{
"title": "Mind-Blowing Performances",
"body": "The cast in this series delivers outstanding performances. Their talent and chemistry bring the characters to life. Bravo!",
"score": 8
},
{
"title": "Lacks Substance",
"body": "Unfortunately, this series lacks depth. The characters are underdeveloped, and the plot feels weak. It didn't leave a lasting impression.",
"score": 4
},
{
"title": "Disappointing Execution",
"body": "I had high expectations for this series, but it fell short. The pacing was off, and the storytelling felt disjointed. A missed opportunity.",
"score": 3
},
{
"title": "Unengaging Storylines",
"body": "The series failed to captivate me. The storylines felt uninteresting, and the episodes dragged on. It didn't hold my attention.",
"score": 5
},
{
"title": "Lackluster Character Development",
"body": "The characters in this series lacked depth and growth. Their arcs felt stagnant, and I struggled to connect with them.",
"score": 4
},
{
"title": "Visually Stunning",
"body": "This series is a visual treat. The cinematography and production design are top-notch. It's a feast for the eyes!",
"score": 9
},
{
"title": "Addictive and Suspenseful",
"body": "I couldn't stop watching this series. Each episode ended with a cliffhanger that left me eagerly waiting for the next. So suspenseful!",
"score": 8
},
{
"title": "Heartwarming and Inspiring",
"body": "This series touched my heart. It's uplifting, inspiring, and reminds us of the power of friendship and resilience. A feel-good watch!",
"score": 9
},
{
"title": "Thought-Provoking and Mind-Bending",
"body": "This series messes with your mind in the best way possible. It raises intriguing questions and challenges your perception of reality.",
"score": 7
},
{
"title": "A Rollercoaster of Emotions",
"body": "This series had me laughing one moment and in tears the next. It's an emotional rollercoaster that leaves a lasting impact.",
"score": 8
},
{
"title": "Captivating Ensemble Cast",
"body": "The chemistry among the cast members is electric. They bring the characters to life with such authenticity and make the series truly engaging.",
"score": 9
},
{
"title": "Disappointing Plot Twists",
"body": "The series relied too heavily on predictable plot twists. It left me underwhelmed and craving for more surprising twists and turns.",
"score": 4
},
{
"title": "Lacks Coherence",
"body": "The series felt disjointed and lacked a cohesive narrative. The storylines didn't seamlessly come together, leaving me confused.",
"score": 3
},
{
"title": "Underdeveloped Supporting Characters",
"body": "While the main characters shined, the supporting cast felt underutilized. Their arcs were shallow and left much to be desired.",
"score": 5
},
{
"title": "Visually Underwhelming",
"body": "The series didn't make good use of its visual potential. The cinematography and production design felt lackluster and uninspired.",
"score": 4
},
{
"title": "Predictable Storylines",
"body": "The series followed familiar tropes and lacked originality. The storylines unfolded exactly as expected, offering little surprises.",
"score": 3
},
{
"title": "A Forgettable Experience",
"body": "Despite its initial promise, this series failed to make a lasting impression. It lacked memorable moments and failed to leave a lasting impact.",
"score": 5
},
{
"title": "Compelling and Addictive",
"body": "This series had me hooked from the first episode. The characters and their complex relationships kept me invested throughout. A must-watch!",
"score": 8
},
{
"title": "Gripping and Intense",
"body": "The series is a thrilling ride from start to finish. The suspense and tension never let up. I couldn't look away!",
"score": 9
},
{
"title": "Relatable and Authentic",
"body": "The characters in this series feel like real people. I could relate to their struggles and triumphs. It's a genuine and authentic portrayal!",
"score": 8
},
{
"title": "Brilliant Ensemble Cast",
"body": "The cast in this series is phenomenal. Each actor brings their A-game, creating a dynamic and captivating ensemble. Brilliant performances!",
"score": 9
},
{
"title": "Underwhelming Plot",
"body": "The series failed to deliver an engaging and cohesive storyline. The plot felt disjointed and left me wanting more depth.",
"score": 4
},
{
"title": "Slow-Paced and Boring",
"body": "The series lacked excitement and dragged on. The slow pace made it difficult to stay engaged. It didn't hold my interest.",
"score": 3
},
{
"title": "Weak Supporting Characters",
"body": "While the main characters were well-developed, the supporting cast felt underdeveloped and served little purpose in the series.",
"score": 5
},
{
"title": "Visually Average",
"body": "The series didn't stand out visually. The cinematography and production design were mediocre, lacking creativity and flair.",
"score": 4
},
{
"title": "Lack of Originality",
"body": "The series felt derivative and failed to bring anything new to the table. It lacked fresh ideas and original storytelling.",
"score": 3
},
{
"title": "Easily Forgettable",
"body": "Despite its initial intrigue, this series didn't leave a lasting impact. It lacked memorable moments and failed to make a lasting impression.",
"score": 5
},
{
"title": "Compelling and Captivating",
"body": "This series had me hooked from the first episode. The intricate plot and complex characters kept me on the edge of my seat. A must-watch!",
"score": 8
},
{
"title": "Thrilling and Heart-Pounding",
"body": "This series had me on the edge of my seat. The suspense and intense moments kept me glued to the screen. So exhilarating!",
"score": 9
},
{
"title": "Authentic Character Development",
"body": "The series beautifully portrayed the growth and development of its characters. I felt invested in their journeys. Truly compelling!",
"score": 8
},
{
"title": "Ensemble Cast Chemistry",
"body": "The chemistry among the cast members is electric. Their interactions and dynamics add depth and richness to the series. Fantastic ensemble!",
"score": 9
},
{
"title": "Weak Plot Progression",
"body": "The series struggled with pacing and failed to progress the plot effectively. It felt stagnant and lacked momentum.",
"score": 4
},
{
"title": "Lacks Excitement",
"body": "The series failed to deliver thrilling moments or exciting plot developments. It left me wanting more action and intensity.",
"score": 3
},
{
"title": "Underutilized Supporting Characters",
"body": "While the main characters shined, the supporting cast felt underused and lacked substantial storylines. Their potential wasn't fully explored.",
"score": 5
},
{
"title": "Visually Unimpressive",
"body": "The series didn't utilize its visual elements to their full potential. The cinematography and production design were uninspiring.",
"score": 4
},
{
"title": "Unoriginal Storylines",
"body": "The series relied on familiar tropes and predictable plotlines. It lacked innovation and failed to offer any surprises.",
"score": 3
},
{
"title": "Easily Forgettable",
"body": "Despite its initial promise, this series didn't leave a lasting impression. It lacked memorable moments and failed to make a lasting impact.",
"score": 5
},
{
"title": "Engrossing and Addictive",
"body": "This series had me captivated from the first episode. The storytelling and character arcs kept me invested throughout. A must-watch!",
"score": 8
},
{
"title": "Nail-Biting and Suspenseful",
"body": "The series had me on the edge of my seat. The suspense and tension were palpable. I couldn't look away!",
"score": 9
},
{
"title": "Well-Developed Characters",
"body": "The series excelled in character development. Each character had depth and their own compelling storylines. Truly engaging!",
"score": 8
},
{
"title": "Chemistry-Driven Ensemble",
"body": "The cast's chemistry is electric. Their interactions and relationships bring an extra layer of authenticity to the series. Outstanding ensemble!",
"score": 9
},
{
"title": "Weak Plot Twists",
"body": "The series failed to deliver impactful plot twists. The twists felt predictable and lacked the wow factor. Disappointing.",
"score": 4
},
{
"title": "Lacks Momentum",
"body": "The series struggled to maintain momentum. It felt slow and didn't build enough excitement or suspense.",
"score": 3
},
{
"title": "Underdeveloped Supporting Cast",
"body": "While the main characters shined, the supporting cast felt underutilized and lacked meaningful character arcs. A missed opportunity.",
"score": 5
},
{
"title": "Visually Unremarkable",
"body": "The series didn't stand out visually. The cinematography and production design were average, lacking innovation and creativity.",
"score": 4
},
{
"title": "Predictable and Clichéd",
"body": "The series followed predictable storylines and clichéd tropes. It failed to bring anything fresh or original to the table.",
"score": 3
},
{
"title": "Easily Forgettable",
"body": "Despite its initial intrigue, this series didn't leave a lasting impact. It lacked memorable moments and failed to make a lasting impression.",
"score": 5
},
{
"title": "Compelling and Addictive",
"body": "This series had me hooked from the first episode. The intricate plot and complex characters kept me on the edge of my seat. A must-watch!",
"score": 8
},
{
"title": "Thrilling and Heart-Pounding",
"body": "This series had me on the edge of my seat. The suspense and intense moments kept me glued to the screen. So exhilarating!",
"score": 9
},
{
"title": "Authentic Character Development",
"body": "The series beautifully portrayed the growth and development of its characters. I felt invested in their journeys. Truly compelling!",
"score": 8
},
{
"title": "Ensemble Cast Chemistry",
"body": "The chemistry among the cast members is electric. Their interactions and dynamics add depth and richness to the series. Fantastic ensemble!",
"score": 9
},
{
"title": "Weak Plot Progression",
"body": "The series struggled with pacing and failed to progress the plot effectively. It felt stagnant and lacked momentum.",
"score": 4
},
{
"title": "Lacks Excitement",
"body": "The series failed to deliver thrilling moments or exciting plot developments. It left me wanting more action and intensity.",
"score": 3
},
{
"title": "Underutilized Supporting Characters",
"body": "While the main characters shined, the supporting cast felt underused and lacked substantial storylines. Their potential wasn't fully explored.",
"score": 5
},
{
"title": "Visually Unimpressive",
"body": "The series didn't utilize its visual elements to their full potential. The cinematography and production design were uninspiring.",
"score": 4
},
{
"title": "Unoriginal Storylines",
"body": "The series relied on familiar tropes and predictable plotlines. It lacked innovation and failed to offer any surprises.",
"score": 3
},
{
"title": "Easily Forgettable",
"body": "Despite its initial promise, this series didn't leave a lasting impression. It lacked memorable moments and failed to make a lasting impact.",
"score": 5
},
{
"title": "Compelling and Captivating",
"body": "This series had me hooked from the first episode. The intricate plot and complex characters kept me on the edge of my seat. A must-watch!",
"score": 8
},
{
"title": "Thrilling and Suspenseful",
"body": "This series kept me on the edge of my seat. The suspense and tension were palpable. I couldn't look away!",
"score": 9
},
{
"title": "Engaging Character Dynamics",
"body": "The series excelled in showcasing the relationships and dynamics between the characters. Their interactions were captivating.",
"score": 8
},
{
"title": "Impressive Ensemble Cast",
"body": "The cast delivered outstanding performances, bringing depth and authenticity to their characters. A stellar ensemble!",
"score": 9
},
{
"title": "Weak Plot Development",
"body": "The series struggled to develop its plot effectively. The storylines felt disjointed and lacked coherence.",
"score": 4
},
{
"title": "Lackluster Excitement",
"body": "The series failed to deliver excitement or thrilling moments. It lacked the necessary suspense and intensity.",
"score": 3
},
{
"title": "Underdeveloped Supporting Cast",
"body": "While the main characters were well-developed, the supporting cast felt underutilized and lacked substantial storylines.",
"score": 5
},
{
"title": "Visually Mediocre",
"body": "The series didn't stand out visually. The cinematography and production design felt average and uninspired.",
"score": 4
},
{
"title": "Predictable and Stale",
"body": "The series followed predictable storylines and failed to offer any surprises. It lacked freshness and originality.",
"score": 3
},
{
"title": "Easily Forgettable",
"body": "Despite its initial intrigue, this series didn't leave a lasting impact. It lacked memorable moments and failed to make a lasting impression.",
"score": 5
},
{
"title": "Addictive and Riveting",
"body": "This series had me hooked from the first episode. The intriguing storyline and compelling characters kept me engaged throughout. Can't wait for the next season!",
"score": 8
},
{
"title": "Heartwarming and Uplifting",
"body": "The series touched my heart and left me feeling inspired. It's a beautiful portrayal of hope, love, and resilience. Highly recommended!",
"score": 9
},
{
"title": "Laugh-Out-Loud Funny",
"body": "This series had me in stitches from start to finish. The humor is clever and the comedic timing is spot-on. A perfect comedy series!",
"score": 8
},
{
"title": "Mind-Blowing Plot Twists",
"body": "Just when I thought I had it all figured out, this series delivered mind-blowing plot twists that left me in awe. It kept me guessing until the very end!",
"score": 9
},
{
"title": "Engaging and Addictive",
"body": "This series grabbed my attention and didn't let go. The fast-paced storytelling and gripping cliffhangers kept me eagerly anticipating the next episode.",
"score": 8
},
{
"title": "Thought-Provoking and Intelligent",
"body": "The series tackles complex themes and presents them with depth and intelligence. It sparks meaningful discussions and leaves a lasting impact.",
"score": 7
},
{
"title": "Unexpected and Surprising",
"body": "This series kept me on my toes with its unexpected plot developments and surprising character arcs. It defied my expectations in the best way!",
"score": 8
},
{
"title": "Gripping and Suspenseful",
"body": "The series had me on the edge of my seat. The tension and suspense were palpable, making it a thrilling and nail-biting experience.",
"score": 9
},
{
"title": "Immersive and Captivating",
"body": "This series transported me to another world. The immersive storytelling and rich world-building kept me fully engaged. A must-watch for fantasy fans!",
"score": 8
},
{
"title": "Powerful and Impactful",
"body": "This series packs an emotional punch. It explores important themes with depth and authenticity, leaving a lasting impact on the viewer.",
"score": 9
},
{
"title": "Addictive and Thrilling",
"body": "I couldn't stop watching this series. Each episode ended with a cliffhanger that left me craving for more. It's a pulse-pounding thrill ride!",
"score": 8
},
{
"title": "Compelling Character Dynamics",
"body": "The relationships between the characters are the heart of this series. The chemistry and complexity make it a truly engaging watch.",
"score": 9
},
{
"title": "Disappointing Plot Development",
"body": "The series failed to progress the plot effectively. It felt slow and lacked the necessary momentum to keep me fully invested.",
"score": 4
},
{
"title": "Underwhelming and Lackluster",
"body": "This series didn't live up to the hype. It lacked excitement and failed to deliver on its promising premise. Disappointing overall.",
"score": 3
},
{
"title": "Weak Characterization",
"body": "The characters in this series felt one-dimensional and lacked depth. Their arcs and motivations were underdeveloped and left me wanting more.",
"score": 5
},
{
"title": "Visually Stunning",
"body": "The series is a visual feast for the eyes. The stunning cinematography and production design create a mesmerizing and immersive experience.",
"score": 9
},
{
"title": "Emotionally Charged",
"body": "This series tugged at my heartstrings. The emotional depth and raw performances left me in tears. A deeply moving experience!",
"score": 8
},
{
"title": "Smart and Mind-Bending",
"body": "The series challenges your perception and keeps you guessing. It's a smartly crafted and mind-bending journey that will leave you in awe.",
"score": 9
},
{
"title": "Heartfelt and Genuine",
"body": "This series touched my heart with its authenticity and heartfelt storytelling. It's a genuine portrayal of human emotions and relationships.",
"score": 8
},
{
"title": "Disappointing Plot Twists",
"body": "The series relied on predictable and uninspired plot twists. It failed to surprise or engage me. The twists felt forced and lacking impact.",
"score": 4
},
{
"title": "Lacks Momentum and Pacing",
"body": "The series struggled with pacing issues and lacked the necessary momentum to keep me fully engaged. It felt slow and dragged on at times.",
"score": 3
},
{
"title": "Underdeveloped Supporting Characters",
"body": "While the main characters shined, the supporting cast felt underutilized and lacked substantial storylines. Their potential wasn't fully realized.",
"score": 5
},
{
"title": "Visually Mediocre",
"body": "The series didn't impress visually. The cinematography and production design felt average and didn't leave a lasting impact.",
"score": 4
},
{
"title": "Predictable and Formulaic",
"body": "The series followed a predictable formula, leaving little room for surprises. It felt formulaic and failed to offer originality.",
"score": 3
},
{
"title": "Easily Forgettable",
"body": "Despite its initial intrigue, this series failed to leave a lasting impression. It lacked memorable moments and failed to make a lasting impact.",
"score": 5
}
]

686
app/Actions/Demo/demo-users.json Executable file
View File

@@ -0,0 +1,686 @@
[
{
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"gender": "male"
},
{
"email": "jane.smith@example.com",
"first_name": "Jane",
"last_name": "Smith",
"gender": "female"
},
{
"email": "michael.johnson@example.com",
"first_name": "Michael",
"last_name": "Johnson",
"gender": "male"
},
{
"email": "emily.williams@example.com",
"first_name": "Emily",
"last_name": "Williams",
"gender": "female"
},
{
"email": "william.brown@example.com",
"first_name": "William",
"last_name": "Brown",
"gender": "male"
},
{
"email": "sophia.jones@example.com",
"first_name": "Sophia",
"last_name": "Jones",
"gender": "female"
},
{
"email": "jackson.taylor@example.com",
"first_name": "Jackson",
"last_name": "Taylor",
"gender": "male"
},
{
"email": "olivia.anderson@example.com",
"first_name": "Olivia",
"last_name": "Anderson",
"gender": "female"
},
{
"email": "matthew.johnson@example.com",
"first_name": "Matthew",
"last_name": "Johnson",
"gender": "male"
},
{
"email": "emma.martin@example.com",
"first_name": "Emma",
"last_name": "Martin",
"gender": "female"
},
{
"email": "liam.thompson@example.com",
"first_name": "Liam",
"last_name": "Thompson",
"gender": "male"
},
{
"email": "ava.hernandez@example.com",
"first_name": "Ava",
"last_name": "Hernandez",
"gender": "female"
},
{
"email": "noah.white@example.com",
"first_name": "Noah",
"last_name": "White",
"gender": "male"
},
{
"email": "isabella.moore@example.com",
"first_name": "Isabella",
"last_name": "Moore",
"gender": "female"
},
{
"email": "ethan.martin@example.com",
"first_name": "Ethan",
"last_name": "Martin",
"gender": "male"
},
{
"email": "mia.lewis@example.com",
"first_name": "Mia",
"last_name": "Lewis",
"gender": "female"
},
{
"email": "mason.hill@example.com",
"first_name": "Mason",
"last_name": "Hill",
"gender": "male"
},
{
"email": "amelia.wilson@example.com",
"first_name": "Amelia",
"last_name": "Wilson",
"gender": "female"
},
{
"email": "logan.clark@example.com",
"first_name": "Logan",
"last_name": "Clark",
"gender": "male"
},
{
"email": "harper.robinson@example.com",
"first_name": "Harper",
"last_name": "Robinson",
"gender": "female"
},
{
"email": "oliver.walker@example.com",
"first_name": "Oliver",
"last_name": "Walker",
"gender": "male"
},
{
"email": "evelyn.cooper@example.com",
"first_name": "Evelyn",
"last_name": "Cooper",
"gender": "female"
},
{
"email": "lucas.peterson@example.com",
"first_name": "Lucas",
"last_name": "Peterson",
"gender": "male"
},
{
"email": "abigail.kelly@example.com",
"first_name": "Abigail",
"last_name": "Kelly",
"gender": "female"
},
{
"email": "aiden.richardson@example.com",
"first_name": "Aiden",
"last_name": "Richardson",
"gender": "male"
},
{
"email": "elizabeth.cook@example.com",
"first_name": "Elizabeth",
"last_name": "Cook",
"gender": "female"
},
{
"email": "michael.ross@example.com",
"first_name": "Michael",
"last_name": "Ross",
"gender": "male"
},
{
"email": "sofia.bennett@example.com",
"first_name": "Sofia",
"last_name": "Bennett",
"gender": "female"
},
{
"email": "alexander.brooks@example.com",
"first_name": "Alexander",
"last_name": "Brooks",
"gender": "male"
},
{
"email": "charlotte.bell@example.com",
"first_name": "Charlotte",
"last_name": "Bell",
"gender": "female"
},
{
"email": "ethan.hall@example.com",
"first_name": "Ethan",
"last_name": "Hall",
"gender": "male"
},
{
"email": "grace.perez@example.com",
"first_name": "Grace",
"last_name": "Perez",
"gender": "female"
},
{
"email": "daniel.baker@example.com",
"first_name": "Daniel",
"last_name": "Baker",
"gender": "male"
},
{
"email": "hannah.bailey@example.com",
"first_name": "Hannah",
"last_name": "Bailey",
"gender": "female"
},
{
"email": "matthew.rogers@example.com",
"first_name": "Matthew",
"last_name": "Rogers",
"gender": "male"
},
{
"email": "ella.gomez@example.com",
"first_name": "Ella",
"last_name": "Gomez",
"gender": "female"
},
{
"email": "joseph.morris@example.com",
"first_name": "Joseph",
"last_name": "Morris",
"gender": "male"
},
{
"email": "chloe.bailey@example.com",
"first_name": "Chloe",
"last_name": "Bailey",
"gender": "female"
},
{
"email": "william.sanchez@example.com",
"first_name": "William",
"last_name": "Sanchez",
"gender": "male"
},
{
"email": "zoey.harris@example.com",
"first_name": "Zoey",
"last_name": "Harris",
"gender": "female"
},
{
"email": "christopher.foster@example.com",
"first_name": "Christopher",
"last_name": "Foster",
"gender": "male"
},
{
"email": "aubrey.patterson@example.com",
"first_name": "Aubrey",
"last_name": "Patterson",
"gender": "female"
},
{
"email": "ryan.bell@example.com",
"first_name": "Ryan",
"last_name": "Bell",
"gender": "male"
},
{
"email": "avery.sullivan@example.com",
"first_name": "Avery",
"last_name": "Sullivan",
"gender": "female"
},
{
"email": "noah.washington@example.com",
"first_name": "Noah",
"last_name": "Washington",
"gender": "male"
},
{
"email": "addison.roberts@example.com",
"first_name": "Addison",
"last_name": "Roberts",
"gender": "female"
},
{
"email": "david.hughes@example.com",
"first_name": "David",
"last_name": "Hughes",
"gender": "male"
},
{
"email": "madison.murphy@example.com",
"first_name": "Madison",
"last_name": "Murphy",
"gender": "female"
},
{
"email": "jacob.edwards@example.com",
"first_name": "Jacob",
"last_name": "Edwards",
"gender": "male"
},
{
"email": "scarlett.watson@example.com",
"first_name": "Scarlett",
"last_name": "Watson",
"gender": "female"
},
{
"email": "michael.russell@example.com",
"first_name": "Michael",
"last_name": "Russell",
"gender": "male"
},
{
"email": "grace.hill@example.com",
"first_name": "Grace",
"last_name": "Hill",
"gender": "female"
},
{
"email": "logan.mitchell@example.com",
"first_name": "Logan",
"last_name": "Mitchell",
"gender": "male"
},
{
"email": "sophia.myers@example.com",
"first_name": "Sophia",
"last_name": "Myers",
"gender": "female"
},
{
"email": "john.richardson@example.com",
"first_name": "John",
"last_name": "Richardson",
"gender": "male"
},
{
"email": "abigail.wilson@example.com",
"first_name": "Abigail",
"last_name": "Wilson",
"gender": "female"
},
{
"email": "william.sanders@example.com",
"first_name": "William",
"last_name": "Sanders",
"gender": "male"
},
{
"email": "lily.howard@example.com",
"first_name": "Lily",
"last_name": "Howard",
"gender": "female"
},
{
"email": "james.hall@example.com",
"first_name": "James",
"last_name": "Hall",
"gender": "male"
},
{
"email": "sophia.gonzalez@example.com",
"first_name": "Sophia",
"last_name": "Gonzalez",
"gender": "female"
},
{
"email": "joseph.russell@example.com",
"first_name": "Joseph",
"last_name": "Russell",
"gender": "male"
},
{
"email": "mia.allen@example.com",
"first_name": "Mia",
"last_name": "Allen",
"gender": "female"
},
{
"email": "alexander.peterson@example.com",
"first_name": "Alexander",
"last_name": "Peterson",
"gender": "male"
},
{
"email": "emily.brooks@example.com",
"first_name": "Emily",
"last_name": "Brooks",
"gender": "female"
},
{
"email": "daniel.stewart@example.com",
"first_name": "Daniel",
"last_name": "Stewart",
"gender": "male"
},
{
"email": "chloe.bennett@example.com",
"first_name": "Chloe",
"last_name": "Bennett",
"gender": "female"
},
{
"email": "lucas.rogers@example.com",
"first_name": "Lucas",
"last_name": "Rogers",
"gender": "male"
},
{
"email": "ava.watson@example.com",
"first_name": "Ava",
"last_name": "Watson",
"gender": "female"
},
{
"email": "mason.howard@example.com",
"first_name": "Mason",
"last_name": "Howard",
"gender": "male"
},
{
"email": "zoey.gonzalez@example.com",
"first_name": "Zoey",
"last_name": "Gonzalez",
"gender": "female"
},
{
"email": "andrew.wood@example.com",
"first_name": "Andrew",
"last_name": "Wood",
"gender": "male"
},
{
"email": "grace.patterson@example.com",
"first_name": "Grace",
"last_name": "Patterson",
"gender": "female"
},
{
"email": "william.perez@example.com",
"first_name": "William",
"last_name": "Perez",
"gender": "male"
},
{
"email": "scarlett.bailey@example.com",
"first_name": "Scarlett",
"last_name": "Bailey",
"gender": "female"
},
{
"email": "ryan.sanchez@example.com",
"first_name": "Ryan",
"last_name": "Sanchez",
"gender": "male"
},
{
"email": "olivia.harris@example.com",
"first_name": "Olivia",
"last_name": "Harris",
"gender": "female"
},
{
"email": "alexander.kelly@example.com",
"first_name": "Alexander",
"last_name": "Kelly",
"gender": "male"
},
{
"email": "aubrey.richardson@example.com",
"first_name": "Aubrey",
"last_name": "Richardson",
"gender": "female"
},
{
"email": "noah.cook@example.com",
"first_name": "Noah",
"last_name": "Cook",
"gender": "male"
},
{
"email": "david.morris@example.com",
"first_name": "David",
"last_name": "Morris",
"gender": "female"
},
{
"email": "amelia.walker@example.com",
"first_name": "Amelia",
"last_name": "Walker",
"gender": "male"
},
{
"email": "jacob.richardson@example.com",
"first_name": "Jacob",
"last_name": "Richardson",
"gender": "female"
},
{
"email": "michael.hill@example.com",
"first_name": "Michael",
"last_name": "Hill",
"gender": "male"
},
{
"email": "sophia.peterson@example.com",
"first_name": "Sophia",
"last_name": "Peterson",
"gender": "female"
},
{
"email": "william.bell@example.com",
"first_name": "William",
"last_name": "Bell",
"gender": "male"
},
{
"email": "zoey.hall@example.com",
"first_name": "Zoey",
"last_name": "Hall",
"gender": "female"
},
{
"email": "christopher.murphy@example.com",
"first_name": "Christopher",
"last_name": "Murphy",
"gender": "male"
},
{
"email": "aubrey.myers@example.com",
"first_name": "Aubrey",
"last_name": "Myers",
"gender": "female"
},
{
"email": "ryan.richardson@example.com",
"first_name": "Ryan",
"last_name": "Richardson",
"gender": "male"
},
{
"email": "addison.wilson@example.com",
"first_name": "Addison",
"last_name": "Wilson",
"gender": "female"
},
{
"email": "david.sanders@example.com",
"first_name": "David",
"last_name": "Sanders",
"gender": "male"
},
{
"email": "madison.hill@example.com",
"first_name": "Madison",
"last_name": "Hill",
"gender": "female"
},
{
"email": "jacob.peterson@example.com",
"first_name": "Jacob",
"last_name": "Peterson",
"gender": "male"
},
{
"email": "scarlett.brooks@example.com",
"first_name": "Scarlett",
"last_name": "Brooks",
"gender": "female"
},
{
"email": "michael.stewart@example.com",
"first_name": "Michael",
"last_name": "Stewart",
"gender": "male"
},
{
"email": "grace.bennett@example.com",
"first_name": "Grace",
"last_name": "Bennett",
"gender": "female"
},
{
"email": "logan.roberts@example.com",
"first_name": "Logan",
"last_name": "Roberts",
"gender": "male"
},
{
"email": "sophia.baker@example.com",
"first_name": "Sophia",
"last_name": "Baker",
"gender": "female"
},
{
"email": "john.gomez@example.com",
"first_name": "John",
"last_name": "Gomez",
"gender": "male"
},
{
"email": "abigail.wood@example.com",
"first_name": "Abigail",
"last_name": "Wood",
"gender": "female"
},
{
"email": "william.patterson@example.com",
"first_name": "William",
"last_name": "Patterson",
"gender": "male"
},
{
"email": "lily.perez@example.com",
"first_name": "Lily",
"last_name": "Perez",
"gender": "female"
},
{
"email": "james.hughes@example.com",
"first_name": "James",
"last_name": "Hughes",
"gender": "male"
},
{
"email": "sophia.murphy@example.com",
"first_name": "Sophia",
"last_name": "Murphy",
"gender": "female"
},
{
"email": "joseph.edwards@example.com",
"first_name": "Joseph",
"last_name": "Edwards",
"gender": "male"
},
{
"email": "mia.washington@example.com",
"first_name": "Mia",
"last_name": "Washington",
"gender": "female"
},
{
"email": "alexander.rogers@example.com",
"first_name": "Alexander",
"last_name": "Rogers",
"gender": "male"
},
{
"email": "emily.gonzalez@example.com",
"first_name": "Emily",
"last_name": "Gonzalez",
"gender": "female"
},
{
"email": "daniel.morris@example.com",
"first_name": "Daniel",
"last_name": "Morris",
"gender": "male"
},
{
"email": "chloe.walker@example.com",
"first_name": "Chloe",
"last_name": "Walker",
"gender": "female"
},
{
"email": "lucas.peterson@example.com",
"first_name": "Lucas",
"last_name": "Peterson",
"gender": "male"
},
{
"email": "ava.watson@example.com",
"first_name": "Ava",
"last_name": "Watson",
"gender": "female"
},
{
"email": "mason.howard@example.com",
"first_name": "Mason",
"last_name": "Howard",
"gender": "male"
},
{
"email": "zoey.gonzalez@example.com",
"first_name": "Zoey",
"last_name": "Gonzalez",
"gender": "female"
}
]

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Actions\Lists;
use App\Models\Channel;
use App\Models\User;
use Common\Database\Datasource\Datasource;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
class ListsLoader
{
public function forUser(User $user, array $params): AbstractPaginator
{
$builder = $user->lists()->where('internal', false);
if (Auth::id() !== $user->id) {
$builder->where('public', true);
}
$pagination = (new Datasource($builder, $params))->paginate();
$pagination
->loadCount('items')
->load([
'items' => fn($query) => $query
->orderBy('order')
->where('order', '<=', 4)
->compact(),
])
->transform(function (Channel $list) {
$list->description = Str::limit($list->description, 80);
return $list;
});
return $pagination;
}
public function allLists(array $params): AbstractPaginator
{
$builder = Channel::where('type', 'list')
->where('internal', false)
->with('user');
$pagination = (new Datasource($builder, $params))->paginate();
$pagination->loadCount('items');
$pagination->transform(function (Channel $list) {
$list->description = Str::limit($list->description, 80);
return $list;
});
return $pagination;
}
}

41
app/Actions/LocalSearch.php Executable file
View File

@@ -0,0 +1,41 @@
<?php
namespace App\Actions;
use App\Actions\People\LoadPrimaryCredit;
use App\Models\Person;
use App\Models\Title;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class LocalSearch
{
public function execute(string $query, array $params = []): Collection
{
$titles = collect();
$people = collect();
if (Arr::get($params, 'type') !== 'person') {
$titles = Title::search($query)
->take(20)
->get();
if ($with = Arr::get($params, 'with')) {
$with = array_filter(explode(',', $with));
$titles->load($with);
}
}
if (Arr::get($params, 'type') !== 'title') {
$people = Person::search($query)
->take(20)
->get();
app(LoadPrimaryCredit::class)->execute($people);
}
return $titles
->concat($people)
->slice(0, Arr::get($params, 'limit', 8))
->values();
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace App\Actions\News;
use App\Models\NewsArticle;
use App\Models\Person;
use App\Models\Title;
use App\Services\Data\News\ImdbNewsProvider;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ImportNewsFromRemoteProvider
{
public function execute(): void
{
$newArticles = collect(
app(ImdbNewsProvider::class)->getArticles(),
)->map(function ($article) {
$article['slug'] = slugify(Str::limit($article['title'], 50));
return $article;
});
$existing = NewsArticle::whereIn(
'slug',
$newArticles->pluck('slug'),
)->get();
// filter out already existing articles
$newArticles = $newArticles
->filter(
fn($newArticle) => !$existing->first(
fn($existingArticle) => $existingArticle['title'] ===
$newArticle['title'] ||
$existingArticle['slug'] === $newArticle['slug'],
),
)
->unique('slug');
$articlesPayload = $newArticles->map(function ($article, $index) {
$date = isset($article['date'])
? Carbon::parse($article['date'])
: Carbon::now();
return [
'title' => $article['title'],
'body' => $article['body'],
'slug' => $article['slug'],
'image' => $article['image'] ?? null,
'source' => $article['source'] ?? null,
'source_url' => $article['source_url'] ?? null,
'byline' => $article['byline'] ?? null,
'created_at' => $date,
'updated_at' => $date,
];
});
NewsArticle::insert($articlesPayload->toArray());
$this->attachArticlesToRelatedModels($newArticles);
}
protected function attachArticlesToRelatedModels(Collection $articles): void
{
// fetch inserted articles from db
$dbArticles = NewsArticle::whereIn(
'slug',
$articles->pluck('slug'),
)->get(['id', 'slug']);
$titleImdbIds = [];
$personImdbIds = [];
// generate [article_slug => [imdb_id, imdb_id, ...]] arrays
foreach ($articles as $article) {
if (isset($article['imdb_title_ids'])) {
foreach ($article['imdb_title_ids'] as $imdbTitleId) {
$titleImdbIds[$article['slug']][] = $imdbTitleId;
}
}
if (isset($article['imdb_person_ids'])) {
foreach ($article['imdb_person_ids'] as $imdbPersonId) {
$personImdbIds[$article['slug']][] = $imdbPersonId;
}
}
}
// fetch titles and people by imdb_id as [imdb_id => id] arrays
$titles = Title::whereIn(
'imdb_id',
collect($titleImdbIds)
->values()
->flatten(),
)
->pluck('id', 'imdb_id')
->toArray();
$people = Person::whereIn(
'imdb_id',
collect($personImdbIds)
->values()
->flatten(),
)
->pluck('id', 'imdb_id')
->toArray();
// generate titles payload for pivot table
$payload = [];
foreach ($titleImdbIds as $slug => $imdbIds) {
foreach ($imdbIds as $imdbId) {
if (isset($titles[$imdbId])) {
$payload[] = [
'article_id' => $dbArticles->firstWhere('slug', $slug)
->id,
'model_id' => $titles[$imdbId],
'model_type' => Title::MODEL_TYPE,
];
}
}
}
// generate people payload for pivot table
foreach ($personImdbIds as $slug => $imdbIds) {
foreach ($imdbIds as $imdbId) {
if (isset($people[$imdbId])) {
$payload[] = [
'article_id' => $dbArticles->firstWhere('slug', $slug)
->id,
'model_id' => $people[$imdbId],
'model_type' => Person::class,
];
}
}
}
DB::table('news_article_models')->insert($payload);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Actions\People;
use App\Models\Person;
use Illuminate\Support\Facades\DB;
class DeletePeople
{
public function execute(array $ids): void
{
Person::withoutGlobalScope('adult')
->whereIn('id', $ids)
->delete();
DB::table('creditables')
->whereIn('person_id', $ids)
->delete();
}
}

View File

@@ -0,0 +1,283 @@
<?php
namespace App\Actions\People;
use App\Models\Person;
use App\Models\Title;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class GetPersonCredits
{
private ?int $titleId;
public function execute(Person $person, $options = []): array
{
$this->titleId = Arr::get($options, 'titleId');
$credits = $this->titleId ? [] : $this->getTitleCredits($person);
$seasonCredits = $this->getSeasonCredits($person);
$episodeCredits = $this->getEpisodeCredits($person);
$mergedCredits = $this->mergeCredits(
Arr::get($credits, 'all', []),
$seasonCredits,
$episodeCredits,
);
$mergedCredits = $this->separateSelfCreditsAndSort($mergedCredits);
return [
'credits' => $mergedCredits,
'knownFor' => Arr::get($credits, 'knownFor', []),
'total_credits_count' => array_reduce(
$mergedCredits,
fn($carry, $item) => $carry + count($item),
0,
),
];
}
private function mergeCredits($credits1, $credits2, $credits3): array
{
$mergedCredits = array_merge_recursive($credits1, $credits2, $credits3);
return array_map(function ($titles) {
// sort titles by year
usort($titles, fn($a, $b) => $b['year'] - $a['year']);
$unique = [];
// if this title already exists and existing
// title has episodes property, continue,
// otherwise push title into 'unique' array
foreach ($titles as $title) {
$existing = Arr::get($unique, $title['id']);
if ($existing) {
$existing['credited_episode_count'] =
Arr::get($existing, 'credited_episode_count', 0) +
Arr::get($title, 'credited_episode_count', 0);
$existing['episodes'] = collect(
array_merge(
Arr::get($existing, 'episodes', []),
Arr::get($title, 'episodes', []),
),
)
->unique('id')
->toArray();
if (!$this->titleId) {
$existing['episodes'] = array_slice(
$existing['episodes'],
0,
5,
);
}
$unique[$title['id']] = $existing;
} else {
$unique[$title['id']] = $title;
}
}
return array_values($unique);
}, $mergedCredits);
}
private function getTitleCredits(Person $person): array
{
$credits = $person->credits()->get();
// generate known for list for actors "known_for" department.
$allKnownFor = $credits
->filter(function (Title $credit) use ($person) {
$knownFor =
strtolower($person->known_for) === 'acting'
? 'actors'
: $person->known_for;
return $credit->pivot->department === strtolower($knownFor);
})
->unique();
$knownFor = $allKnownFor->where('pivot.order', '<', 10);
if ($knownFor->count() < 4) {
$knownFor = $allKnownFor;
}
// sort by person credit "order" for title as well as title popularity
$knownFor = $knownFor
->sortBy(function ($title) {
$order = $title->pivot->order;
$popularity = $title->popularity;
return $order - $popularity;
})
->slice(0, 6)
->values();
// cast to array, so poster/backdrop is not removed later.
$knownFor = $knownFor->load(['primaryVideo'])->toArray();
// remove any data not needed to render person filmography
$credits = $credits
->map(function (Title $credit) {
unset($credit['backdrop']);
return $credit;
})
->groupBy('pivot.department');
return ['all' => $credits->toArray(), 'knownFor' => $knownFor];
}
/**
* Get credits for all series seasons person is attached to.
*/
private function getSeasonCredits(Person $person): array
{
$seasons = $person
->seasonCredits($this->titleId)
->with([
'title' => fn($query) => $query->select(
'id',
'name',
'release_date',
'poster',
),
'episodes' => fn($query) => $query
->select(
'id',
'name',
'release_date',
'season_id',
'season_number',
'episode_number',
'title_id',
)
->orderBy('season_number', 'desc')
->orderBy('episode_number', 'desc'),
])
->get();
// group all seasons by department, for example "production"
$groupedSeasons = $seasons->groupBy('pivot.department');
return $groupedSeasons
->map(function (Collection $departmentGroup) {
$seasonsGroupedByTitle = $departmentGroup->groupBy('title.id');
// attach episodes from all seasons to title
return $seasonsGroupedByTitle
->map(function (Collection $titleSeasons) {
$title = $titleSeasons
->first(fn($s) => $s->title)
->title->toArray();
//get episodes from each season and move season "pivot" data to each episode
$episodesFromAllSeasons = $titleSeasons
->pluck('episodes')
->flatten()
->values()
->map(function ($episode) use ($titleSeasons) {
$episode->pivot = $titleSeasons
->first()
->pivot->toArray();
return $episode;
});
$title[
'credited_episode_count'
] = $episodesFromAllSeasons->count();
if (!$this->titleId) {
$episodesFromAllSeasons = $episodesFromAllSeasons->take(
5,
);
}
$title['episodes'] = $episodesFromAllSeasons->toArray();
return $title;
})
->values();
})
->toArray();
}
/**
* Get all individual episodes person is credited for.
*
* This will return array grouped by department, and
* series with all episodes person is credited for attached
* to that series.
*
* @param Person $person
* @return array
*/
private function getEpisodeCredits(Person $person)
{
$episodes = $person
->episodeCredits($this->titleId)
->with([
'title' => function (BelongsTo $query) {
$query->select('id', 'name', 'release_date', 'poster');
},
])
->get();
$groupedByDep = $episodes->groupBy('pivot.department');
return $groupedByDep
->map(
fn(Collection $episodes) => $episodes
->groupBy('title.id')
->map(function (Collection $episodes) {
if (!$episodes->first()->title) {
return null;
}
$title = $episodes->first()->title->toArray();
$episodes = $episodes->map(function ($episode) {
unset($episode->title);
return $episode;
});
$title['credited_episode_count'] = $episodes->count();
if (!$this->titleId) {
$episodes = $episodes->take(5);
}
$title['episodes'] = $episodes->toArray();
return $title;
})
->filter()
->values(),
)
->toArray();
}
private function separateSelfCreditsAndSort(array $credits): array
{
if (!isset($credits['cast'])) {
return $credits;
}
$cast = [];
$self = [];
foreach ($credits['cast'] as $credit) {
$char = isset($credit['pivot']['character'])
? strtolower($credit['pivot']['character'])
: null;
if (
$char &&
($char === 'self' || Str::contains($char, 'himself'))
) {
$self[] = $credit;
} else {
$cast[] = $credit;
}
}
$credits['cast'] = $cast;
// sort before adding "self" to array as that should be last always
uksort(
$credits,
fn($a, $b) => (is_countable($credits[$b])
? count($credits[$b])
: 0) - (is_countable($credits[$a]) ? count($credits[$a]) : 0),
);
$credits['self'] = $self;
return $credits;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Actions\People;
use App\Models\Title;
use Illuminate\Database\QueryException;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class LoadPrimaryCredit
{
public function execute(Collection|AbstractPaginator $people): void
{
$prefix = DB::getTablePrefix();
$titleSelect = Title::select([
'titles.id',
'titles.is_series',
'titles.name',
DB::raw("{$prefix}creditables.person_id as pivot_person_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(
"row_number() over (partition by {$prefix}creditables.person_id order by {$prefix}titles.popularity desc) as laravel_row",
),
])
->join('creditables', 'titles.id', '=', 'creditables.creditable_id')
->where('creditables.creditable_type', Title::MODEL_TYPE)
->whereIn('creditables.person_id', $people->pluck('id'))
// this scope will mess up binding merging below
->withoutGlobalScope('adult')
->when(
!config('tmdb.includeAdult'),
fn($q) => $q->where('adult', false),
);
// cache syntax error, if mysql version does not support partition
try {
$items = DB::table(
DB::raw("({$titleSelect->toSql()}) as laravel_table"),
)
->select('*')
->mergeBindings($titleSelect->getQuery())
->where('laravel_row', '<=', 1)
->orderBy('laravel_row')
->get();
} catch (QueryException $e) {
return;
}
$credits = Title::hydrate($items->toArray());
$people->each(function ($person) use ($credits) {
$credit = $credits->first(
fn($credit) => $credit->pivot_person_id === $person->id,
);
if ($credit) {
$person->primary_credit = [
'id' => $credit->id,
'is_series' => $credit->is_series,
'name' => $credit->name,
'year' => $credit->year,
'model_type' => $credit->model_type,
];
}
});
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Actions\People;
use App\Models\Person;
use Common\Database\Datasource\Datasource;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class PaginatePeople
{
public function execute(array $params, $builder = null): AbstractPaginator
{
$builder = $builder ?? Person::query();
$isCompact = Arr::get($params, 'compact');
if ($isCompact) {
$builder->select([
'id',
'name',
'birth_date',
'death_date',
'poster',
'popularity',
]);
}
$datasource = new Datasource($builder, $params);
// prevent duplicate items when ordering by columns that are not
// guaranteed to be unique (name, popularity, age etc.)
$datasource->secondaryOrderCol = 'id';
if (!Arr::get($params, 'order') && !Arr::get($params, 'orderBy')) {
$datasource->order = [
'col' => 'popularity',
'dir' => 'desc',
];
}
if (
$datasource->getOrder()['col'] === 'popularity' &&
($min = config('content.people_index_min_popularity'))
) {
$builder->where('popularity', '>', $min);
}
$pagination = $datasource->paginate();
if (!$isCompact) {
$pagination->transform(function (Person $person) {
$person->description = Str::limit($person->description, 500);
return $person;
});
//app(LoadPrimaryCredit::class)->execute($pagination);
}
return $pagination;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Actions\People;
use App\Actions\Titles\Store\StoreCredits;
use App\Actions\Titles\StoresMediaImages;
use App\Models\Person;
class StorePersonData
{
use StoresMediaImages;
private ?Person $person = null;
private ?array $data = null;
public function execute(Person $person, array $data): Person
{
$this->person = $person;
$this->data = $data;
$this->persistData();
$this->persistRelations();
return $this->person;
}
private function persistData(): void
{
$personData = array_filter($this->data, function (
$value, // make sure we don't overwrite existing values with null
) {
if (is_array($value)) {
return false;
}
// if fully_synced is true, override everything and erase any previously set values.
// For example if "death_date" was previously set on a person and tmdb now returns null for "death_date", set "death_date" to null in database.
if (
config('common.site.tmdb_delete_when_sync') &&
$this->data['fully_synced']
) {
return true;
}
// if "tmdb_delete_when_sync" is false, don't clear existing values, as values set from admin manually might be erased
return !is_null($value);
});
$this->person->fill($personData)->save();
}
private function persistRelations(): void
{
$relations = array_filter($this->data, fn($value) => is_array($value));
foreach ($relations as $name => $values) {
switch ($name) {
case 'credits':
app(StoreCredits::class)->execute($this->person, $values);
break;
case 'images':
$this->storeImages($values, $this->person);
break;
}
}
}
}

View File

@@ -0,0 +1,399 @@
<?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,
],
],
];
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Actions\Plays;
use App\Models\Episode;
use App\Models\Season;
use App\Models\Title;
use App\Models\User;
use Common\Admin\Analytics\Actions\BuildDemoAnalyticsReport;
use Common\Admin\Analytics\Actions\DemoTrend;
use Common\Database\Metrics\MetricDateRange;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
class GenerateFakePlaysData
{
public function playsTrend(Builder $builder, MetricDateRange $range): array
{
return (new DemoTrend($builder, dateRange: $range))->count();
}
public function partitionMetric(string $groupBy): array
{
$method =
'build' .
Str::of($groupBy)
->ucfirst()
->plural()
->toString() .
'Metric';
return (new BuildDemoAnalyticsReport())->$method();
}
public function titles(bool $isSeries = null): array
{
return Title::orderBy('popularity', 'desc')
->when(
!is_null($isSeries),
fn($query) => $query->where('is_series', $isSeries),
)
->where('language', 'en')
->limit(30)
->compact()
->get()
->map(
fn(Title $title) => [
'label' => $title->name,
'value' => random_int(50, 1654),
'percentage' => random_int(1, 100),
'model' => $title,
],
)
->sortByDesc('value')
->values()
->toArray();
}
public function seasons(int $titleId): array
{
return Season::where('title_id', $titleId)
->with(['title' => fn($query) => $query->compact()])
->get()
->map(function (Season $season) {
return [
'label' => $season['name'],
'value' => random_int(50, 1654),
'percentage' => random_int(1, 100),
'model' => $season,
];
})
->sortByDesc('value')
->values()
->toArray();
}
public function episodes(int $titleId): array
{
return Episode::where('title_id', $titleId)
->with(['title' => fn($query) => $query->compact()])
->get()
->map(function (Episode $episode) {
return [
'label' => $episode['name'],
'value' => random_int(50, 1654),
'percentage' => random_int(1, 100),
'model' => $episode,
];
})
->sortByDesc('value')
->values()
->toArray();
}
public function videos(): array
{
return Title::orderBy('popularity', 'desc')
->where('language', 'en')
->limit(30)
->compact()
->with('videos')
->get()
->filter(fn(Title $title) => $title->videos->isNotEmpty())
->map(function (Title $title) {
$video = $title->videos->random()->toArray();
$title->unsetRelation('videos');
$video['title'] = $title->toArray();
return [
'label' => $video['name'],
'value' => random_int(50, 1654),
'percentage' => random_int(1, 100),
'model' => $video,
];
})
->sortByDesc('value')
->values()
->toArray();
}
public function users(): array
{
return User::inRandomOrder()
->limit(30)
->compact()
->get()
->map(
fn(User $user) => [
'label' => $user->display_name,
'value' => random_int(50, 1654),
'percentage' => random_int(1, 100),
'model' => $user,
],
)
->sortByDesc('value')
->values()
->toArray();
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Actions\Plays;
use App\Models\Video;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Jenssegers\Agent\Facades\Agent;
class LogVideoPlay
{
public function execute(Video $video, array $params = []): void
{
if (isset($params['currentTime'])) {
$this->updateTimeWatched($video, $params);
} else {
$this->logVideoPlay($video);
}
}
private function updateTimeWatched(Video $video, array $params): void
{
$lastPlay = $video
->plays()
->forCurrentUser()
->orderBy('created_at', 'desc')
->first();
$timeWatched = round($params['currentTime']);
$duration = round($params['duration']);
// if user watched over 95%, we can assume video is fully watched
$fullyWatched = $timeWatched >= (95 / 100) * $duration;
// if fully watched or watched less than 60 seconds, set time watched to 0
if ($fullyWatched || $timeWatched < 60) {
$timeWatched = 0;
}
if ($lastPlay) {
$lastPlay
->fill([
'time_watched' => $timeWatched,
'duration' => round($params['duration']),
])
->save();
}
}
private function logVideoPlay(Video $video): void
{
if (!$this->alreadyLoggedToday($video)) {
$ip = getIp();
$video->plays()->create([
'location' => $this->getLocation($ip),
'platform' => strtolower(Agent::platform()),
'device' => $this->getDevice(),
'browser' => strtolower(Agent::browser()),
'user_id' => Auth::id(),
'ip' => $ip,
]);
}
}
protected function alreadyLoggedToday(Video $video): bool
{
return $video
->plays()
->forCurrentUser()
->whereBetween('created_at', [
Carbon::now()->subDay(),
Carbon::now(),
])
->exists();
}
protected function getDevice(): string
{
if (Agent::isMobile()) {
return 'mobile';
} elseif (Agent::isTablet()) {
return 'tablet';
} else {
return 'desktop';
}
}
protected function getLocation(string $ip): string
{
return strtolower(geoip($ip)['iso_code']);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Actions\Reviews;
use App\Models\Review;
use Illuminate\Support\Facades\DB;
class UpdateReviewableAverageScore
{
public function execute(int $reviewableId, string $reviewableType): void
{
$votes = app(Review::class)
->where('reviewable_type', $reviewableType)
->where('reviewable_id', $reviewableId)
->select(
DB::raw('avg(`score`) as average'),
DB::raw('count(*) as count'),
)
->first();
$average = number_format((float) $votes['average'], 1);
// title or episode
$model = app(modelTypeToNamespace($reviewableType))->find(
$reviewableId,
);
$model->local_vote_average = $average;
$model->local_vote_count = $votes['count'];
$model->save();
}
}

View 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();
}
}

View 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();
}
}

View 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];
}
}

View 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);
}
}

View 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();
}
}

View 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;
}
}
}

View 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();
}
}

View 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');
}
}

View 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;
}
}

View 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));
}
}

View 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();
}
}

View 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;
}
}

View 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'));
}
}

View 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);
}
}
}

View 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,
);
}
}

View 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());
}
}

View 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);
}
}

View 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);
}
}

View 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');
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Actions\Videos;
use App\Models\Episode;
use App\Models\Video;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
class CrupdateVideo
{
public function execute(array $params, int $videoId = null): Video
{
if (Arr::get($params, 'episode_num')) {
$episode = Episode::where('title_id', $params['title_id'])
->where('episode_number', $params['episode_num'])
->where('season_number', $params['season_num'])
->firstOrFail();
$params['episode_id'] = $episode->id;
}
$params['positive_votes'] = 0;
$params['negative_votes'] = 0;
$params['origin'] = 'local';
$captions = Arr::pull($params, 'captions');
if ($videoId) {
$video = Video::findOrFail($videoId);
$video->fill($params)->save();
} else {
$params['approved'] = $this->shouldAutoApprove();
$params['user_id'] = Auth::id();
$video = Video::create($params);
}
if (isset($captions)) {
$this->syncCaptions($video, $captions);
}
return $video;
}
private function shouldAutoApprove(): bool
{
return settings('streaming.auto_approve') ||
Auth::user()->hasPermission('admin');
}
protected function syncCaptions(Video $video, array $captions): void
{
$captions = collect($captions);
// delete captions that were removed
$captionIds = $captions->pluck('id')->filter();
$video
->captions()
->whereNotIn('id', $captionIds)
->delete();
$captions->each(function ($caption, $index) use ($video) {
if (isset($caption['id'])) {
$video
->captions()
->where('id', $caption['id'])
->update([
'name' => $caption['name'],
'language' => $caption['language'],
'url' => $caption['url'],
'order' => $index,
]);
} else {
$caption['user_id'] = Auth::id();
$caption['order'] = $index;
$video->captions()->create($caption);
}
});
}
}