18
.editorconfig
Executable file
18
.editorconfig
Executable file
@@ -0,0 +1,18 @@
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
quote_type = single
|
||||
max_line_length = 80
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.php]
|
||||
indent_size = 4
|
||||
59
.env.example
Executable file
59
.env.example
Executable file
@@ -0,0 +1,59 @@
|
||||
APP_NAME=MTDb
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:NE5wdzVEQm9CeUVMTGRtczNEZXRuVjFaeVQ0QmJTQUc=
|
||||
APP_DEBUG=true
|
||||
APP_LOG_LEVEL=debug
|
||||
APP_URL=http://localhost
|
||||
APP_TIMEZONE=UTC
|
||||
APP_LOCALE=english
|
||||
APP_VERSION=4.0.4
|
||||
DISABLE_UPDATE_AUTH=true
|
||||
BILLING_ENABLED=true
|
||||
INSTALLED=false
|
||||
LOCAL_SEARCH_MODE=fulltext
|
||||
ENABLE_CONTACT_PAGE=true
|
||||
NOTIFICATIONS_ENABLED=true
|
||||
SCOUT_DRIVER=mysql
|
||||
SCOUT_MYSQL_MODE=extended
|
||||
SCOUT_PREFIX=mtdb_
|
||||
API_INTEGRATED=true
|
||||
CLOCKWORK_ENABLED=false
|
||||
PULSE_ENABLED=false
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=172.28.2.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=mtdb
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
DB_STRICT_MODE=false
|
||||
DB_PREFIX=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
|
||||
SENTRY_DSN=null
|
||||
|
||||
RATING_COLUMN=tmdb_vote_average
|
||||
|
||||
STATIC_FILE_DELIVERY=null
|
||||
PUBLIC_DISK_DRIVER=local
|
||||
|
||||
PAYPAL_PRODUCT_ID=b808bbf3-7372-4e57-b6fd-7672a546bd61
|
||||
PAYPAL_PRODUCT_NAME="MTDb Subscription"
|
||||
64
.eslintrc.json
Executable file
64
.eslintrc.json
Executable file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 13,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react", "react-hooks", "@typescript-eslint"],
|
||||
"ignorePatterns": ["*.config.*", "*.d.ts"],
|
||||
"rules": {
|
||||
"react/destructuring-assignment": "off",
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react/no-array-index-key": "off",
|
||||
"react/jsx-filename-extension": [
|
||||
1,
|
||||
{
|
||||
"extensions": [".tsx"]
|
||||
}
|
||||
],
|
||||
"import/prefer-default-export": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"radix": ["error", "as-needed"],
|
||||
"prefer-const": [
|
||||
"error",
|
||||
{
|
||||
"destructuring": "all"
|
||||
}
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
"no-plusplus": "off",
|
||||
"arrow-body-style": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/lines-between-class-members": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"@typescript-eslint/no-unused-expressions": "error",
|
||||
"no-param-reassign": "off",
|
||||
"class-methods-use-this": "off",
|
||||
"consistent-return": "off",
|
||||
"default-case": "off",
|
||||
"no-return-assign": ["error", "except-parens"],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/require-default-props": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react/jsx-fragments": "off",
|
||||
"react/function-component-definition": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"react/display-name": "off",
|
||||
"react/no-unstable-nested-components": ["error", {"allowAsProps": true}]
|
||||
}
|
||||
}
|
||||
5
.gitattributes
vendored
Executable file
5
.gitattributes
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
* text=auto
|
||||
*.css linguist-vendored
|
||||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
CHANGELOG.md export-ignore
|
||||
33
.github/workflows/build.yaml
vendored
Executable file
33
.github/workflows/build.yaml
vendored
Executable file
@@ -0,0 +1,33 @@
|
||||
name: Build
|
||||
on: [push]
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'true'
|
||||
token: ${{ secrets.GHUB_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build client
|
||||
run: npm run build
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_KEY }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET }}
|
||||
aws-region: us-west-1
|
||||
|
||||
- name: Deploy build assets to S3 bucket
|
||||
run: aws s3 sync ./public/build/ s3://vebto-assets/mtdb/build --delete
|
||||
|
||||
#- name: Deploy vendor assets to S3 bucket
|
||||
#run: aws s3 sync ./public/vendor/ s3://vebto-assets/mtdb/vendor --delete
|
||||
|
||||
50
.gitignore
vendored
Executable file
50
.gitignore
vendored
Executable file
@@ -0,0 +1,50 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
vendor
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# misc
|
||||
/storage/tntsearch/*.index
|
||||
/storage/framework/maintenance.php
|
||||
.htaccess
|
||||
public/.htaccess
|
||||
public/favicon
|
||||
public/favicon.ico
|
||||
install_files
|
||||
/public/hot
|
||||
/storage/*.key
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
.env
|
||||
data.ms
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
storageState.json
|
||||
# translation files (include default "en"), will be created during installation
|
||||
resources/lang/*.json
|
||||
|
||||
# Sentry Auth Token
|
||||
.env.sentry-build-plugin
|
||||
3
.gitmodules
vendored
Executable file
3
.gitmodules
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
[submodule "common"]
|
||||
path = common
|
||||
url = git@github.com:RamunasO/common-new.git
|
||||
2
.prettierignore
Executable file
2
.prettierignore
Executable file
@@ -0,0 +1,2 @@
|
||||
**/routes/*.php
|
||||
*.min.css
|
||||
19
.prettierrc.yaml
Executable file
19
.prettierrc.yaml
Executable file
@@ -0,0 +1,19 @@
|
||||
arrowParens: avoid
|
||||
bracketSpacing: false
|
||||
phpVersion: "8.0"
|
||||
singleQuote: true
|
||||
|
||||
plugins:
|
||||
- "@prettier/plugin-php"
|
||||
- "prettier-plugin-tailwindcss"
|
||||
- "prettier-plugin-blade"
|
||||
|
||||
overrides:
|
||||
- files: "*.html"
|
||||
options:
|
||||
printWidth: 100
|
||||
tabWidth: 2
|
||||
singleQuote: false
|
||||
- files: "*.php"
|
||||
options:
|
||||
tabWidth: 4
|
||||
193
app/Actions/AppValueLists.php
Executable file
193
app/Actions/AppValueLists.php
Executable 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();
|
||||
}
|
||||
}
|
||||
138
app/Actions/Channels/FetchContentFromLocalDatabase.php
Executable file
138
app/Actions/Channels/FetchContentFromLocalDatabase.php
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
160
app/Actions/Channels/FetchContentFromTmdb.php
Executable file
160
app/Actions/Channels/FetchContentFromTmdb.php
Executable 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
97
app/Actions/Demo/GenerateDemoAnimeVideos.php
Executable file
97
app/Actions/Demo/GenerateDemoAnimeVideos.php
Executable 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;
|
||||
}
|
||||
}
|
||||
131
app/Actions/Demo/GenerateDemoComments.php
Executable file
131
app/Actions/Demo/GenerateDemoComments.php
Executable 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();
|
||||
}
|
||||
}
|
||||
100
app/Actions/Demo/GenerateDemoReviews.php
Executable file
100
app/Actions/Demo/GenerateDemoReviews.php
Executable 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();
|
||||
}
|
||||
}
|
||||
185
app/Actions/Demo/GenerateDemoStreamVideos.php
Executable file
185
app/Actions/Demo/GenerateDemoStreamVideos.php
Executable 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
53
app/Actions/Demo/GenerateDemoUsers.php
Executable file
53
app/Actions/Demo/GenerateDemoUsers.php
Executable 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());
|
||||
}
|
||||
}
|
||||
41
app/Actions/Demo/GenerateDemoVideoVotes.php
Executable file
41
app/Actions/Demo/GenerateDemoVideoVotes.php
Executable 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();
|
||||
}
|
||||
}
|
||||
161
app/Actions/Demo/demo-anime-comments.json
Executable file
161
app/Actions/Demo/demo-anime-comments.json
Executable 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!"
|
||||
}
|
||||
]
|
||||
365
app/Actions/Demo/demo-episode-comments.json
Executable file
365
app/Actions/Demo/demo-episode-comments.json
Executable 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!"
|
||||
}
|
||||
]
|
||||
356
app/Actions/Demo/demo-movie-comments.json
Executable file
356
app/Actions/Demo/demo-movie-comments.json
Executable 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!"
|
||||
}
|
||||
]
|
||||
523
app/Actions/Demo/demo-movie-reviews.json
Executable file
523
app/Actions/Demo/demo-movie-reviews.json
Executable 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
|
||||
}
|
||||
]
|
||||
|
||||
329
app/Actions/Demo/demo-series-comments.json
Executable file
329
app/Actions/Demo/demo-series-comments.json
Executable 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!"
|
||||
}
|
||||
]
|
||||
507
app/Actions/Demo/demo-series-reviews.json
Executable file
507
app/Actions/Demo/demo-series-reviews.json
Executable 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
686
app/Actions/Demo/demo-users.json
Executable 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"
|
||||
}
|
||||
]
|
||||
56
app/Actions/Lists/ListsLoader.php
Executable file
56
app/Actions/Lists/ListsLoader.php
Executable 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
41
app/Actions/LocalSearch.php
Executable 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();
|
||||
}
|
||||
}
|
||||
136
app/Actions/News/ImportNewsFromRemoteProvider.php
Executable file
136
app/Actions/News/ImportNewsFromRemoteProvider.php
Executable 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);
|
||||
}
|
||||
}
|
||||
19
app/Actions/People/DeletePeople.php
Executable file
19
app/Actions/People/DeletePeople.php
Executable 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();
|
||||
}
|
||||
}
|
||||
283
app/Actions/People/GetPersonCredits.php
Executable file
283
app/Actions/People/GetPersonCredits.php
Executable 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;
|
||||
}
|
||||
}
|
||||
74
app/Actions/People/LoadPrimaryCredit.php
Executable file
74
app/Actions/People/LoadPrimaryCredit.php
Executable 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,
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
62
app/Actions/People/PaginatePeople.php
Executable file
62
app/Actions/People/PaginatePeople.php
Executable 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;
|
||||
}
|
||||
}
|
||||
68
app/Actions/People/StorePersonData.php
Executable file
68
app/Actions/People/StorePersonData.php
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
399
app/Actions/Plays/BuildPlaysReport.php
Executable file
399
app/Actions/Plays/BuildPlaysReport.php
Executable 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,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
137
app/Actions/Plays/GenerateFakePlaysData.php
Executable file
137
app/Actions/Plays/GenerateFakePlaysData.php
Executable 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();
|
||||
}
|
||||
}
|
||||
92
app/Actions/Plays/LogVideoPlay.php
Executable file
92
app/Actions/Plays/LogVideoPlay.php
Executable 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']);
|
||||
}
|
||||
}
|
||||
32
app/Actions/Reviews/UpdateReviewableAverageScore.php
Executable file
32
app/Actions/Reviews/UpdateReviewableAverageScore.php
Executable 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();
|
||||
}
|
||||
}
|
||||
38
app/Actions/Titles/DeleteSeasons.php
Executable file
38
app/Actions/Titles/DeleteSeasons.php
Executable file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Season;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DeleteSeasons
|
||||
{
|
||||
public function execute(array|Collection $seasonIds): void
|
||||
{
|
||||
// seasons
|
||||
DB::table('creditables')
|
||||
->whereIn('creditable_id', $seasonIds)
|
||||
->where('creditable_type', Season::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
app(Season::class)
|
||||
->whereIn('id', $seasonIds)
|
||||
->delete();
|
||||
|
||||
// episodes
|
||||
$episodeIds = app(Episode::class)
|
||||
->whereIn('season_id', $seasonIds)
|
||||
->pluck('id');
|
||||
|
||||
DB::table('creditables')
|
||||
->whereIn('creditable_id', $episodeIds)
|
||||
->where('creditable_type', Episode::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
app(Episode::class)
|
||||
->whereIn('id', $episodeIds)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
104
app/Actions/Titles/DeleteTitles.php
Executable file
104
app/Actions/Titles/DeleteTitles.php
Executable file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Image;
|
||||
use App\Models\Listable;
|
||||
use App\Models\Review;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use App\Models\Video;
|
||||
use Common\Comments\Comment;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DeleteTitles
|
||||
{
|
||||
public function execute(array $titleIds): void
|
||||
{
|
||||
$seasonIds = app(Season::class)
|
||||
->whereIn('title_id', $titleIds)
|
||||
->pluck('id');
|
||||
app(DeleteSeasons::class)->execute($seasonIds);
|
||||
|
||||
// credits
|
||||
DB::table('creditables')
|
||||
->whereIn('creditable_id', $titleIds)
|
||||
->where('creditable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// images
|
||||
app(Image::class)
|
||||
->whereIn('model_id', $titleIds)
|
||||
->where('model_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// list items
|
||||
app(Listable::class)
|
||||
->whereIn('listable_id', $titleIds)
|
||||
->where('listable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// channel items
|
||||
DB::table('channelables')
|
||||
->whereIn('channelable_id', $titleIds)
|
||||
->where('channelable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// reviews
|
||||
Review::whereIn('reviewable_id', $titleIds)
|
||||
->where('reviewable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// comments
|
||||
Comment::whereIn('commentable_id', $titleIds)
|
||||
->where('commentable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// tags
|
||||
DB::table('taggables')
|
||||
->whereIn('taggable_id', $titleIds)
|
||||
->where('taggable_type', Title::MODEL_TYPE)
|
||||
->delete();
|
||||
|
||||
// keywords
|
||||
DB::table('keyword_title')
|
||||
->whereIn('title_id', $titleIds)
|
||||
->delete();
|
||||
|
||||
// countries
|
||||
DB::table('country_title')
|
||||
->whereIn('title_id', $titleIds)
|
||||
->delete();
|
||||
|
||||
// genres
|
||||
DB::table('genre_title')
|
||||
->whereIn('title_id', $titleIds)
|
||||
->delete();
|
||||
|
||||
// videos
|
||||
$videoIds = app(Video::class)
|
||||
->whereIn('title_id', $titleIds)
|
||||
->pluck('id');
|
||||
app(Video::class)
|
||||
->whereIn('id', $videoIds)
|
||||
->delete();
|
||||
|
||||
DB::table('video_votes')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->delete();
|
||||
DB::table('video_captions')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->delete();
|
||||
DB::table('video_reports')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->delete();
|
||||
DB::table('video_plays')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->delete();
|
||||
|
||||
// titles
|
||||
Title::withoutGlobalScope('adult')
|
||||
->whereIn('id', $titleIds)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
40
app/Actions/Titles/HandlesEncodedTmdbId.php
Executable file
40
app/Actions/Titles/HandlesEncodedTmdbId.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
trait HandlesEncodedTmdbId
|
||||
{
|
||||
public static function encodeTmdbId(
|
||||
string $provider,
|
||||
string $type,
|
||||
string|int $id,
|
||||
): string {
|
||||
return base64_encode("$provider|$type|$id");
|
||||
}
|
||||
|
||||
public static function decodeTmdbId($encodedId): array
|
||||
{
|
||||
$encodedId = base64_decode($encodedId);
|
||||
$parts = explode('|', $encodedId);
|
||||
if (count($parts) === 3) {
|
||||
return [
|
||||
'provider' => $parts[0],
|
||||
'type' => $parts[1],
|
||||
'id' => $parts[2],
|
||||
];
|
||||
} else {
|
||||
return ['provider' => null, 'type' => null, 'id' => null];
|
||||
}
|
||||
}
|
||||
|
||||
public static function decodeTmdbIdOrFail(string $encodedId): array
|
||||
{
|
||||
[$provider, $type, $id] = array_values(
|
||||
static::decodeTmdbId($encodedId),
|
||||
);
|
||||
if (!$provider || !$type || !$id) {
|
||||
abort(404);
|
||||
}
|
||||
return [(int) $id, $type];
|
||||
}
|
||||
}
|
||||
75
app/Actions/Titles/HasCreditableRelation.php
Executable file
75
app/Actions/Titles/HasCreditableRelation.php
Executable file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Person;
|
||||
use DB;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
trait HasCreditableRelation
|
||||
{
|
||||
public function credits(): BelongsToMany
|
||||
{
|
||||
$query = $this->morphToMany(Person::class, 'creditable')->withPivot([
|
||||
'id',
|
||||
'job',
|
||||
'department',
|
||||
'order',
|
||||
'character',
|
||||
]);
|
||||
|
||||
$query = $query->select(['people.id', 'name', 'poster']);
|
||||
|
||||
// order by department first, so we always get director,
|
||||
// writers and creators, even if limit is applied to this query
|
||||
$prefix = DB::getTablePrefix();
|
||||
return $query
|
||||
->orderBy(
|
||||
DB::raw(
|
||||
"FIELD(department, 'directing', 'creators', 'writing', 'actors')",
|
||||
),
|
||||
)
|
||||
// should be "desc" and not "asc" because "minus" is added which will reverse order
|
||||
->orderBy(DB::raw("-{$prefix}creditables.order"), 'desc');
|
||||
}
|
||||
|
||||
public function updateCredit(int $pivotId, array $payload): void
|
||||
{
|
||||
// lowercase payload
|
||||
$payload = collect($payload)
|
||||
->mapWithKeys(function ($value, $key) {
|
||||
if ($key === 'department' || $key === 'job') {
|
||||
$value = strtolower($value);
|
||||
}
|
||||
return [$key => $value];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
$this->credits()
|
||||
->newPivotQuery()
|
||||
->where('id', $pivotId)
|
||||
->update($payload);
|
||||
}
|
||||
|
||||
public function createCredit(array $payload): void
|
||||
{
|
||||
// lowercase payload
|
||||
$payload = collect($payload)
|
||||
->mapWithKeys(function ($value, $key) {
|
||||
if ($key === 'department' || $key === 'job') {
|
||||
$value = strtolower($value);
|
||||
}
|
||||
return [$key => $value];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
if ($payload['department'] === 'actors') {
|
||||
$payload['order'] =
|
||||
$this->credits()
|
||||
->wherePivot('department', 'actors')
|
||||
->count() + 1;
|
||||
}
|
||||
|
||||
$this->credits()->attach($payload['person_id'], $payload);
|
||||
}
|
||||
}
|
||||
56
app/Actions/Titles/HasVideoRelation.php
Executable file
56
app/Actions/Titles/HasVideoRelation.php
Executable file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
trait HasVideoRelation
|
||||
{
|
||||
public function videos(): HasMany
|
||||
{
|
||||
return $this->hasMany(Video::class)->applySelectedSort();
|
||||
}
|
||||
|
||||
public function primaryVideo(): HasOne
|
||||
{
|
||||
$preferFull = settings('streaming.prefer_full');
|
||||
return $this->hasOne(Video::class)
|
||||
->when($preferFull, function ($query) {
|
||||
$query->where('category', 'full');
|
||||
})
|
||||
->select([
|
||||
'id',
|
||||
'title_id',
|
||||
'name',
|
||||
'category',
|
||||
'episode_id',
|
||||
'season_num',
|
||||
'episode_num',
|
||||
])
|
||||
->when(
|
||||
// is series or movie and not specific episode
|
||||
fn() => static::class !== Episode::class && $preferFull,
|
||||
function ($builder) {
|
||||
return $builder->where(function ($builder) {
|
||||
// video attached directly to movie or series
|
||||
$builder
|
||||
->where(
|
||||
fn($builder) => $builder
|
||||
->whereNull('season_num')
|
||||
->whereNull('episode_num'),
|
||||
)
|
||||
// video attached to first episode of series
|
||||
->orWhere(
|
||||
fn($builder) => $builder
|
||||
->where('season_num', 1)
|
||||
->where('episode_num', 1),
|
||||
);
|
||||
});
|
||||
},
|
||||
)
|
||||
->applySelectedSort();
|
||||
}
|
||||
}
|
||||
53
app/Actions/Titles/InsertsTmdbTitleOrPerson.php
Executable file
53
app/Actions/Titles/InsertsTmdbTitleOrPerson.php
Executable file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Person;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
trait InsertsTmdbTitleOrPerson
|
||||
{
|
||||
public function insertOrRetrieve(Collection $tmdbItems): Collection
|
||||
{
|
||||
$tmdbItems = $tmdbItems->map(function ($value) {
|
||||
unset($value['relation_data']);
|
||||
unset($value['model_type']);
|
||||
unset($value['id']);
|
||||
return $value;
|
||||
});
|
||||
|
||||
$select =
|
||||
static::class === Person::class
|
||||
? ['id', 'tmdb_id', 'name']
|
||||
: ['id', 'tmdb_id', 'name', 'is_series'];
|
||||
|
||||
$existing = $this->withoutGlobalScope('adult')
|
||||
->select($select)
|
||||
->whereIn('tmdb_id', $tmdbItems->pluck('tmdb_id'))
|
||||
->get()
|
||||
->mapWithKeys(fn($item) => [$item['tmdb_id'] => $item]);
|
||||
|
||||
$new = $tmdbItems
|
||||
->filter(fn($item) => !isset($existing[$item['tmdb_id']]))
|
||||
->values();
|
||||
|
||||
if ($new->isNotEmpty()) {
|
||||
$new->transform(function ($item) {
|
||||
$item['created_at'] = Arr::get(
|
||||
$item,
|
||||
'created_at',
|
||||
Carbon::now(),
|
||||
);
|
||||
return $item;
|
||||
});
|
||||
$this->insert($new->toArray());
|
||||
return $this->whereIn('tmdb_id', $tmdbItems->pluck('tmdb_id'))
|
||||
->select($select)
|
||||
->get();
|
||||
} else {
|
||||
return $existing;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
app/Actions/Titles/LoadSeasonEpisodeNumbers.php
Executable file
18
app/Actions/Titles/LoadSeasonEpisodeNumbers.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class LoadSeasonEpisodeNumbers
|
||||
{
|
||||
public function execute(int $titleId, int $seasonNumber): array
|
||||
{
|
||||
return DB::table('episodes')
|
||||
->where('title_id', $titleId)
|
||||
->where('season_number', $seasonNumber)
|
||||
->orderBy('episode_number', 'asc')
|
||||
->pluck('episode_number')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
54
app/Actions/Titles/Retrieve/GetRelatedTitles.php
Executable file
54
app/Actions/Titles/Retrieve/GetRelatedTitles.php
Executable file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GetRelatedTitles
|
||||
{
|
||||
public function execute(Title $title, array $params = []): Collection
|
||||
{
|
||||
$titleIds = $this->getByTags($title, $params);
|
||||
|
||||
if ($titleIds->isNotEmpty()) {
|
||||
return Title::whereIn('id', $titleIds)
|
||||
->with(['primaryVideo'])
|
||||
->when(isset($params['compact']), fn($q) => $q->compact())
|
||||
->get();
|
||||
}
|
||||
|
||||
return collect();
|
||||
}
|
||||
|
||||
private function getByTags(Title $title, array $params): Collection
|
||||
{
|
||||
$prefix = DB::getTablePrefix();
|
||||
$keywordIds = $title->keywords->pluck('id');
|
||||
$genreIds = $title->genres->pluck('id');
|
||||
|
||||
return DB::table('titles')
|
||||
->join('keyword_title as k', 'k.title_id', '=', 'titles.id')
|
||||
->join('genre_title as g', 'g.title_id', '=', 'titles.id')
|
||||
->select(
|
||||
DB::raw(
|
||||
"{$prefix}titles.id, COUNT({$prefix}k.id) + COUNT({$prefix}g.id) as total_count",
|
||||
),
|
||||
)
|
||||
->whereIn('k.keyword_id', $keywordIds)
|
||||
->whereIn('g.genre_id', $genreIds)
|
||||
->when($title->release_date, function ($q) use ($title) {
|
||||
$q->whereBetween('release_date', [
|
||||
$title->release_date->subYears(5)->format('Y-m-d'),
|
||||
$title->release_date->addYears(5)->format('Y-m-d'),
|
||||
]);
|
||||
})
|
||||
->where('titles.id', '!=', $title->id)
|
||||
->groupBy('titles.id')
|
||||
->orderBy('total_count', 'desc')
|
||||
->limit(Arr::get($params, 'limit', 10))
|
||||
->pluck('titles.id');
|
||||
}
|
||||
}
|
||||
40
app/Actions/Titles/Retrieve/PaginateSeasonEpisodes.php
Executable file
40
app/Actions/Titles/Retrieve/PaginateSeasonEpisodes.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PaginateSeasonEpisodes
|
||||
{
|
||||
public function execute(
|
||||
Title $title,
|
||||
int $seasonNumber,
|
||||
array $params = [],
|
||||
): AbstractPaginator {
|
||||
$builder = $title->episodes()->where('season_number', $seasonNumber);
|
||||
$builder->with(['primaryVideo']);
|
||||
|
||||
$orderBy = Arr::get($params, 'orderBy', 'episode_number');
|
||||
$orderDir = Arr::get($params, 'orderDir', 'asc');
|
||||
|
||||
$pagination = $builder
|
||||
->orderBy($orderBy, $orderDir)
|
||||
->simplePaginate(Arr::get($params, 'perPage', 30));
|
||||
|
||||
// only show first paragraph of description
|
||||
$pagination->through(function ($episode) use ($params) {
|
||||
$episode->description = explode("\n", $episode->description)[0];
|
||||
if (Arr::get($params, 'excludeDescription')) {
|
||||
$episode->description = null;
|
||||
} elseif (Arr::get($params, 'truncateDescriptions')) {
|
||||
$episode->description = Str::limit($episode->description, 200);
|
||||
}
|
||||
return $episode;
|
||||
});
|
||||
|
||||
return $pagination;
|
||||
}
|
||||
}
|
||||
26
app/Actions/Titles/Retrieve/PaginateTitleSeasons.php
Executable file
26
app/Actions/Titles/Retrieve/PaginateTitleSeasons.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class PaginateTitleSeasons
|
||||
{
|
||||
public function execute(Title $title, array $params = []): AbstractPaginator
|
||||
{
|
||||
return $title
|
||||
->seasons()
|
||||
->select([
|
||||
'seasons.id',
|
||||
'seasons.poster',
|
||||
'seasons.release_date',
|
||||
'number',
|
||||
'title_id',
|
||||
])
|
||||
->withCount('episodes')
|
||||
->orderBy('number', 'desc')
|
||||
->paginate(Arr::get($params, 'perPage', 8));
|
||||
}
|
||||
}
|
||||
54
app/Actions/Titles/Retrieve/PaginateTitles.php
Executable file
54
app/Actions/Titles/Retrieve/PaginateTitles.php
Executable file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PaginateTitles
|
||||
{
|
||||
public function execute(array $params, $builder = null): AbstractPaginator
|
||||
{
|
||||
if (!$builder) {
|
||||
$builder = Title::query();
|
||||
}
|
||||
|
||||
if ($type = Arr::get($params, 'type')) {
|
||||
$builder->where('is_series', $type === 'series');
|
||||
}
|
||||
|
||||
$builder->with(['primaryVideo']);
|
||||
|
||||
$datasource = new Datasource($builder, $params);
|
||||
|
||||
// prevent duplicate items when ordering by columns that are not
|
||||
// guaranteed to be unique (budget, popularity , revenue etc.)
|
||||
$datasource->secondaryOrderCol = 'id';
|
||||
|
||||
// only show titles with more than 50 votes when filtering by rating
|
||||
if ($datasource->filters->has('tmdb_vote_average')) {
|
||||
$builder->where('tmdb_vote_count', '>=', 50);
|
||||
}
|
||||
|
||||
$order = $datasource->getOrder();
|
||||
|
||||
$order['col'] = str_replace(
|
||||
['user_score', 'rating'],
|
||||
config('common.site.rating_column'),
|
||||
$order['col'],
|
||||
);
|
||||
|
||||
// show titles with less than 50 votes on tmdb last, regardless of their average
|
||||
if (Str::contains($order['col'], 'tmdb_vote_average')) {
|
||||
$builder->orderBy(DB::raw('tmdb_vote_count > 100'), 'desc');
|
||||
}
|
||||
|
||||
$datasource->order = $order;
|
||||
|
||||
return $datasource->paginate();
|
||||
}
|
||||
}
|
||||
55
app/Actions/Titles/Retrieve/ShowTitle.php
Executable file
55
app/Actions/Titles/Retrieve/ShowTitle.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Retrieve;
|
||||
|
||||
use App\Models\Title;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ShowTitle
|
||||
{
|
||||
public function execute(int|string $id, array $params): array
|
||||
{
|
||||
if (defined('SHOULD_PRERENDER')) {
|
||||
$params['skipUpdating'] = true;
|
||||
$params['load'] =
|
||||
'images,genres,productionCountries,keywords,videos,primaryVideo,seasons,compactCredits';
|
||||
$params['loadCount'] = 'seasons';
|
||||
}
|
||||
|
||||
if (is_numeric($id) || ctype_digit($id)) {
|
||||
$title = Title::findOrFail($id);
|
||||
} else {
|
||||
$title = Title::firstOrCreateFromEncodedTmdbId($id);
|
||||
}
|
||||
|
||||
if (!Arr::get($params, 'skipUpdating')) {
|
||||
$title = $title->maybeUpdateFromExternal();
|
||||
if (!$title) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
$response = ['title' => $title->loadCount('seasons')];
|
||||
|
||||
foreach (explode(',', Arr::get($params, 'load', '')) as $relation) {
|
||||
$methodName = sprintf('load%s', Str::camel($relation));
|
||||
if (method_exists($this, $methodName)) {
|
||||
$response = $this->$methodName($title, $params, $response);
|
||||
} elseif (method_exists($title, $relation)) {
|
||||
$title->load($relation);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (
|
||||
explode(',', Arr::get($params, 'loadCount', ''))
|
||||
as $relation
|
||||
) {
|
||||
if (method_exists($title, $relation)) {
|
||||
$title->loadCount($relation);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
209
app/Actions/Titles/Store/StoreCredits.php
Executable file
209
app/Actions/Titles/Store/StoreCredits.php
Executable file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Store;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Person;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StoreCredits
|
||||
{
|
||||
public const UNIQUE_KEY = 'tmdb_id';
|
||||
|
||||
private Title|Episode|Season|Person|null $model = null;
|
||||
|
||||
public function __construct(private Person $person, private Title $title)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $originalMediaItems
|
||||
*/
|
||||
public function execute(Title|Episode|Season|Person $model, $originalMediaItems)
|
||||
{
|
||||
$this->model = $model;
|
||||
|
||||
if (empty($originalMediaItems)) return;
|
||||
|
||||
// generate records we will insert into "creditables" pivot table
|
||||
$newPivotRecords = $this->generatePivotRecords($originalMediaItems);
|
||||
if ($newPivotRecords->isEmpty()) return;
|
||||
|
||||
// fetch all existing "creditable" pivot rows for this creditable
|
||||
$existingPivotRecords = $this->getExistingRecords($newPivotRecords);
|
||||
|
||||
// merge new and existing pivot records, so "order" column value is not lost
|
||||
$mergedPivotRecords = $this->mergeNewAndExistingRecords($existingPivotRecords, $newPivotRecords);
|
||||
|
||||
// delete all "creditables" pivot table records for this creditable
|
||||
$this->detachExistingRecords($existingPivotRecords);
|
||||
|
||||
// insert new pivot records
|
||||
DB::table('creditables')->insert($mergedPivotRecords->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $originalMediaItems
|
||||
* @return Collection
|
||||
*/
|
||||
private function generatePivotRecords($originalMediaItems)
|
||||
{
|
||||
// need to remove series, otherwise it will attach all guest starts to main series, because
|
||||
// tmdb API does not specify whether credit belongs to series, season or episode.
|
||||
$originalMediaItems = $this->filterOutSeries($originalMediaItems);
|
||||
$dbMediaItems = $this->insertOrRetrieveMediaItems($originalMediaItems);
|
||||
|
||||
return collect($originalMediaItems)->map(function($originalMediaItem) use($dbMediaItems) {
|
||||
$creditable = $dbMediaItems->first(fn($item, $uniqueKey) => $uniqueKey === $originalMediaItem[self::UNIQUE_KEY] && $item['is_series'] === Arr::get($originalMediaItem, 'is_series'));
|
||||
|
||||
if ( ! $creditable) return null;
|
||||
|
||||
// either attaching multiple titles to a person
|
||||
// or attaching multiple people to title/season/episode
|
||||
if ($this->model->model_type === Person::MODEL_TYPE) {
|
||||
$personId = $this->model->id;
|
||||
$creditableId = $creditable->id;
|
||||
} else {
|
||||
$personId = $creditable->id;
|
||||
$creditableId = $this->model->id;
|
||||
}
|
||||
|
||||
// build relation records for attaching all credits to title
|
||||
// (same person might be attached to title multiple times)
|
||||
return [
|
||||
'id' => null,
|
||||
'person_id' => $personId,
|
||||
'creditable_id' => $creditableId,
|
||||
'creditable_type' => $this->getCreditableType(),
|
||||
'character' => Arr::get($originalMediaItem, 'relation_data.character'),
|
||||
'order' => Arr::get($originalMediaItem, 'relation_data.order'),
|
||||
'department' => Arr::get($originalMediaItem, 'relation_data.department'),
|
||||
'job' => Arr::get($originalMediaItem, 'relation_data.job'),
|
||||
];
|
||||
})->filter()->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge existing "creditables" pivot table records with the ones
|
||||
* we are about to insert. This needs to be done because sometimes
|
||||
* "order" property does not exist on "new" records, but exists on
|
||||
* "old" ones and since we will delete "old" ones, "order" would be lost.
|
||||
*
|
||||
* @param Collection $existingRecords
|
||||
* @param Collection $newRecords
|
||||
* @return Collection
|
||||
*/
|
||||
private function mergeNewAndExistingRecords($existingRecords, $newRecords)
|
||||
{
|
||||
// all of these properties on both arrays must match for them to be considered equal
|
||||
$matchProps = collect(['person_id', 'creditable_id', 'creditable_type', 'character', 'department', 'job']);
|
||||
|
||||
return $newRecords->map(function($newRecord) use($existingRecords, $matchProps) {
|
||||
$oldRecord = $existingRecords->first(fn($existingRecord) => $matchProps->every(fn($prop) => $existingRecord[$prop] === $newRecord[$prop]), []);
|
||||
return $this->arrayMergeIfNotNull($newRecord, $oldRecord);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $arr1
|
||||
* @param array $arr2
|
||||
* @return array
|
||||
*/
|
||||
private function arrayMergeIfNotNull($arr1, $arr2) {
|
||||
foreach($arr2 as $key => $val) {
|
||||
$is_set_and_not_null = isset($arr1[$key]);
|
||||
if ( $val == null && $is_set_and_not_null ) {
|
||||
$arr2[$key] = $arr1[$key];
|
||||
}
|
||||
}
|
||||
return array_merge($arr1, $arr2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or retrieve titles or people that need to be attached.
|
||||
*
|
||||
* @param Collection|(Title|Person)[] $mediaItems
|
||||
* @return Collection
|
||||
*/
|
||||
private function insertOrRetrieveMediaItems(Collection|array $mediaItems)
|
||||
{
|
||||
// make sure we only insert person/title once, even
|
||||
// if they appear multiple time in title credits
|
||||
$uniqueMediaItems = collect($mediaItems)->unique(self::UNIQUE_KEY)->values();
|
||||
|
||||
if ($uniqueMediaItems->isEmpty()) return collect();
|
||||
|
||||
if ($uniqueMediaItems[0]['model_type'] === Person::MODEL_TYPE) {
|
||||
$mediaItems = $this->person->insertOrRetrieve($uniqueMediaItems);
|
||||
} else {
|
||||
$mediaItems = $this->title->insertOrRetrieve($uniqueMediaItems);
|
||||
}
|
||||
|
||||
return $mediaItems->mapWithKeys(fn($item) => [$item[self::UNIQUE_KEY] => $item]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $records
|
||||
* @return Collection
|
||||
*/
|
||||
private function getExistingRecords($records)
|
||||
{
|
||||
// select only fields needed to do the diff
|
||||
return DB::table('creditables')
|
||||
->whereIn('person_id', $records->pluck('person_id'))
|
||||
->whereIn('job', $records->pluck('job'))
|
||||
->where('creditable_type', $records[0]['creditable_type'])
|
||||
->whereIn('creditable_id', $records->pluck('creditable_id'))
|
||||
->get()
|
||||
->map(fn($record) => (array) $record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete creditable records that already exist.
|
||||
*
|
||||
* @param Collection $records
|
||||
*/
|
||||
private function detachExistingRecords($records)
|
||||
{
|
||||
if ($records->isEmpty()) return;
|
||||
|
||||
// select only fields needed to do the diff
|
||||
$existingRecords = DB::table('creditables')
|
||||
->whereIn('person_id', $records->pluck('person_id'))
|
||||
->whereIn('job', $records->pluck('job'))
|
||||
->where('creditable_type', $records[0]['creditable_type'])
|
||||
->whereIn('creditable_id', $records->pluck('creditable_id'))
|
||||
->get();
|
||||
|
||||
DB::table('creditables')->whereIn('id', $existingRecords->pluck('id'))->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get creditable type for morphToMany or morphedByMany relation.
|
||||
*/
|
||||
private function getCreditableType(): string
|
||||
{
|
||||
if ($this->model::class === Person::class) {
|
||||
// won't be attaching seasons or episodes here
|
||||
// so can just return title type instantly
|
||||
return Title::MODEL_TYPE;
|
||||
} else {
|
||||
return $this->model::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all series and episodes from specified array.
|
||||
*
|
||||
* @param array $originalMediaItems
|
||||
* @return array
|
||||
*/
|
||||
private function filterOutSeries($originalMediaItems)
|
||||
{
|
||||
return array_filter($originalMediaItems, fn($creditable) => !Arr::get($creditable, 'is_series'));
|
||||
}
|
||||
}
|
||||
51
app/Actions/Titles/Store/StoreEpisodeData.php
Executable file
51
app/Actions/Titles/Store/StoreEpisodeData.php
Executable file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Store;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class StoreEpisodeData
|
||||
{
|
||||
public function execute(Title $title, Season $season, $episodes): Title
|
||||
{
|
||||
$existingEpisodes = $season->episodes()->get();
|
||||
foreach ($episodes as $episodeData) {
|
||||
$episode = $this->storePrimaryData(
|
||||
$season,
|
||||
$existingEpisodes,
|
||||
$episodeData,
|
||||
);
|
||||
app(StoreCredits::class)->execute($episode, $episodeData['cast']);
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
|
||||
private function storePrimaryData(
|
||||
Season $season,
|
||||
Collection $existingEpisodes,
|
||||
array $episodeData,
|
||||
): Episode {
|
||||
$episodeData = array_filter(
|
||||
$episodeData,
|
||||
fn($value) => !is_array($value) && $value !== Episode::MODEL_TYPE,
|
||||
);
|
||||
$episodeData['title_id'] = $season->title_id;
|
||||
unset($episodeData['id']);
|
||||
|
||||
$existingEpisode = $existingEpisodes->firstWhere(
|
||||
'episode_number',
|
||||
$episodeData['episode_number'],
|
||||
);
|
||||
|
||||
if ($existingEpisode) {
|
||||
$existingEpisode->update($episodeData);
|
||||
return $existingEpisode;
|
||||
} else {
|
||||
return $season->episodes()->create($episodeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
app/Actions/Titles/Store/StoreSeasonData.php
Executable file
56
app/Actions/Titles/Store/StoreSeasonData.php
Executable file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Store;
|
||||
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
|
||||
class StoreSeasonData
|
||||
{
|
||||
private ?Title $title = null;
|
||||
|
||||
public function execute(Title $title, array $data): Title
|
||||
{
|
||||
if (empty($data)) {
|
||||
return $title;
|
||||
}
|
||||
|
||||
$this->title = $title;
|
||||
|
||||
$season = $this->persistData($data);
|
||||
if (isset($data['cast'])) {
|
||||
app(StoreCredits::class)->execute($season, $data['cast']);
|
||||
}
|
||||
|
||||
if (isset($data['episodes'])) {
|
||||
app(StoreEpisodeData::class)->execute(
|
||||
$title,
|
||||
$season,
|
||||
$data['episodes'],
|
||||
);
|
||||
}
|
||||
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
private function persistData(array $data): Season
|
||||
{
|
||||
// remove all relation data
|
||||
$data = array_filter(
|
||||
$data,
|
||||
fn($value) => !is_array($value) && $value !== Season::MODEL_TYPE,
|
||||
);
|
||||
|
||||
// if season data did not change then timestamps
|
||||
// will not be updated because model is not dirty
|
||||
$data['updated_at'] = now();
|
||||
|
||||
return Season::updateOrCreate(
|
||||
[
|
||||
'title_id' => $this->title->id,
|
||||
'number' => $data['number'],
|
||||
],
|
||||
$data,
|
||||
);
|
||||
}
|
||||
}
|
||||
141
app/Actions/Titles/Store/StoreTitleData.php
Executable file
141
app/Actions/Titles/Store/StoreTitleData.php
Executable file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles\Store;
|
||||
|
||||
use App\Actions\Titles\StoresMediaImages;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StoreTitleData
|
||||
{
|
||||
use StoresMediaImages;
|
||||
|
||||
private ?Title $title = null;
|
||||
|
||||
private ?array $data = null;
|
||||
|
||||
private ?array $options = null;
|
||||
|
||||
public function execute(
|
||||
Title $title,
|
||||
array $data,
|
||||
array $options = [],
|
||||
): Title {
|
||||
$this->title = $title;
|
||||
$this->data = $data;
|
||||
$this->options = $options;
|
||||
|
||||
$this->persistData();
|
||||
$this->persistRelations();
|
||||
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
private function persistData(): void
|
||||
{
|
||||
$titleData = array_filter(
|
||||
$this->data,
|
||||
fn(
|
||||
$value, // make sure we don't overwrite existing values with null
|
||||
) => !is_array($value) &&
|
||||
($this->options['overrideWithEmptyValues'] ?? !is_null($value)),
|
||||
);
|
||||
|
||||
$this->title->fill($titleData)->save();
|
||||
}
|
||||
|
||||
private function persistRelations(): void
|
||||
{
|
||||
$relations = array_filter($this->data, fn($value) => is_array($value));
|
||||
|
||||
foreach ($relations as $name => $values) {
|
||||
switch ($name) {
|
||||
case 'videos':
|
||||
$this->persistVideos($values);
|
||||
break;
|
||||
case 'images':
|
||||
$this->storeImages($values, $this->title);
|
||||
break;
|
||||
case 'genres':
|
||||
$this->persistTags($values, 'genre');
|
||||
break;
|
||||
case 'countries':
|
||||
$this->persistTags($values, 'production_country');
|
||||
break;
|
||||
case 'cast':
|
||||
app(StoreCredits::class)->execute($this->title, $values);
|
||||
break;
|
||||
case 'keywords':
|
||||
$this->persistTags($values, 'keyword');
|
||||
break;
|
||||
case 'seasons':
|
||||
$this->persistSeasons($values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function persistSeasons(array $seasons): void
|
||||
{
|
||||
$newSeasons = collect($seasons)
|
||||
->map(function ($season) {
|
||||
$season['title_id'] = $this->title->id;
|
||||
return $season;
|
||||
})
|
||||
->filter(
|
||||
fn($season) => !$this->title->seasons->contains(
|
||||
'number',
|
||||
$season['number'],
|
||||
),
|
||||
);
|
||||
|
||||
if ($newSeasons->isNotEmpty()) {
|
||||
Season::insert($newSeasons->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
private function persistTags(array $tags, string $type): void
|
||||
{
|
||||
$values = collect($tags)->map(
|
||||
fn($tag) => [
|
||||
'name' => $tag['name'],
|
||||
'display_name' => Arr::get(
|
||||
$tag,
|
||||
'display_name',
|
||||
ucfirst($tag['name']),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
$tags = app(modelTypeToNamespace($type))->insertOrRetrieve(
|
||||
$values,
|
||||
$type,
|
||||
);
|
||||
|
||||
$relation = $this->title->{Str::camel(Str::plural($type))}();
|
||||
$relation->syncWithoutDetaching($tags->pluck('id'));
|
||||
}
|
||||
|
||||
private function persistVideos(array $values): void
|
||||
{
|
||||
$videos = collect($values)
|
||||
->unique(fn($v) => strtolower($v['name']))
|
||||
->values()
|
||||
->map(function ($value, $i) {
|
||||
$value['title_id'] = $this->title->id;
|
||||
$value['order'] = $i + 1;
|
||||
$value['created_at'] = now();
|
||||
$value['updated_at'] = now();
|
||||
return $value;
|
||||
});
|
||||
|
||||
Video::where('origin', '!=', 'local')
|
||||
->where('title_id', $this->title->id)
|
||||
->whereNull('episode_num')
|
||||
->delete();
|
||||
|
||||
Video::insert($videos->toArray());
|
||||
}
|
||||
}
|
||||
47
app/Actions/Titles/StoreMediaImageOnDisk.php
Executable file
47
app/Actions/Titles/StoreMediaImageOnDisk.php
Executable file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class StoreMediaImageOnDisk
|
||||
{
|
||||
// sizes should be ordered by size (desc), to avoid blurry images
|
||||
private array $sizes = [
|
||||
'original' => null,
|
||||
'large' => 500,
|
||||
'medium' => 300,
|
||||
'small' => 92,
|
||||
];
|
||||
|
||||
public function execute(UploadedFile $file): string
|
||||
{
|
||||
$hash = Str::random(30);
|
||||
|
||||
$manager = new ImageManager(new Driver());
|
||||
$img = $manager->read($file);
|
||||
|
||||
$extension = $file->extension() ?? 'jpeg';
|
||||
|
||||
foreach ($this->sizes as $key => $size) {
|
||||
if ($size) {
|
||||
$img->scale($size);
|
||||
}
|
||||
|
||||
Storage::disk('public')->put(
|
||||
"media-images/backdrops/$hash/$key.$extension",
|
||||
$extension === 'png' ? $img->toPng() : $img->toJpeg(),
|
||||
);
|
||||
}
|
||||
|
||||
$endpoint = config('common.site.file_preview_endpoint');
|
||||
$uri = "media-images/backdrops/$hash/original.$extension";
|
||||
return $endpoint
|
||||
? "$endpoint/storage/$uri"
|
||||
: Storage::disk('public')->url($uri);
|
||||
}
|
||||
}
|
||||
24
app/Actions/Titles/StoresMediaImages.php
Executable file
24
app/Actions/Titles/StoresMediaImages.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Person;
|
||||
use App\Models\Title;
|
||||
|
||||
trait StoresMediaImages
|
||||
{
|
||||
public function storeImages(array $values, Title|Person $model): void
|
||||
{
|
||||
$values = array_map(function ($value) use ($model) {
|
||||
$value['model_id'] = $model->id;
|
||||
$value['model_type'] = $model->getMorphClass();
|
||||
return $value;
|
||||
}, $values);
|
||||
|
||||
$model
|
||||
->images()
|
||||
->where('source', '!=', 'local')
|
||||
->delete();
|
||||
$model->images()->insert($values);
|
||||
}
|
||||
}
|
||||
164
app/Actions/Titles/TitleCredits.php
Executable file
164
app/Actions/Titles/TitleCredits.php
Executable file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Titles;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Person;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TitleCredits
|
||||
{
|
||||
public function loadCompact(
|
||||
Title $title,
|
||||
Season $season = null,
|
||||
Episode $episode = null,
|
||||
): Collection {
|
||||
$builder = $this->getBaseQuery($title, $season, $episode)
|
||||
->whereIn('creditables.department', [
|
||||
'actors',
|
||||
'writing',
|
||||
'directing',
|
||||
'creators',
|
||||
])
|
||||
->limit(50);
|
||||
|
||||
$credits = $this->groupByDepartment($builder);
|
||||
|
||||
if (isset($credits['actors'])) {
|
||||
$credits['actors'] = $credits['actors']->take(15)->values();
|
||||
}
|
||||
if (isset($credits['writing'])) {
|
||||
$credits['writing'] = $credits['writing']->take(3)->values();
|
||||
}
|
||||
if (isset($credits['directing'])) {
|
||||
$credits['directing'] = $credits['directing']
|
||||
->filter(fn($c) => $c['pivot']['job'] === 'director')
|
||||
->take(3)
|
||||
->values();
|
||||
}
|
||||
if (isset($credits['creators'])) {
|
||||
$credits['creators'] = $credits['creators']->take(3)->values();
|
||||
}
|
||||
|
||||
return $credits;
|
||||
}
|
||||
|
||||
public function loadFull(
|
||||
Title $title,
|
||||
Season $season = null,
|
||||
Episode $episode = null,
|
||||
): Collection {
|
||||
$builder = $this->getBaseQuery($title, $season, $episode);
|
||||
return $this->groupByDepartment($builder);
|
||||
}
|
||||
|
||||
protected function getBaseQuery(
|
||||
Title $title = null,
|
||||
Season $season = null,
|
||||
Episode $episode = null,
|
||||
): Builder {
|
||||
$prefix = DB::getTablePrefix();
|
||||
$builder = Person::join(
|
||||
'creditables',
|
||||
'people.id',
|
||||
'=',
|
||||
'creditables.person_id',
|
||||
)
|
||||
->select([
|
||||
'people.id',
|
||||
'people.name',
|
||||
'people.poster',
|
||||
DB::raw("{$prefix}creditables.id as pivot_id"),
|
||||
DB::raw(
|
||||
"{$prefix}creditables.creditable_id as pivot_creditable_id",
|
||||
),
|
||||
DB::raw(
|
||||
"{$prefix}creditables.creditable_type as pivot_creditable_type",
|
||||
),
|
||||
DB::raw("{$prefix}creditables.job as pivot_job"),
|
||||
DB::raw("{$prefix}creditables.department as pivot_department"),
|
||||
DB::raw("{$prefix}creditables.order as pivot_order"),
|
||||
DB::raw("{$prefix}creditables.character as pivot_character"),
|
||||
])
|
||||
// Need to keep same person in different departments (as actor and producer for example)
|
||||
// while still removing duplicates when loading credits for title, season and episode
|
||||
->groupBy(['people.id', 'creditables.department']);
|
||||
|
||||
$builder->where(function ($q) use ($title, $season, $episode) {
|
||||
if ($title) {
|
||||
$q->orWhere(
|
||||
fn($q) => $q
|
||||
->where('creditables.creditable_id', $title->id)
|
||||
->where(
|
||||
'creditables.creditable_type',
|
||||
$title->getMorphClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($episode) {
|
||||
$q->orWhere(
|
||||
fn($q) => $q
|
||||
->where('creditables.creditable_id', $episode->id)
|
||||
->where(
|
||||
'creditables.creditable_type',
|
||||
$episode->getMorphClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($season) {
|
||||
$q->orWhere(
|
||||
fn($q) => $q
|
||||
->where('creditables.creditable_id', $season->id)
|
||||
->where(
|
||||
'creditables.creditable_type',
|
||||
$season->getMorphClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// order by department first, so we always get director,
|
||||
// writers and creators, even if limit is applied to this query
|
||||
$prefix = DB::getTablePrefix();
|
||||
return $builder
|
||||
->orderBy(
|
||||
DB::raw(
|
||||
"FIELD(department, 'directing', 'creators', 'writing', 'actors')",
|
||||
),
|
||||
)
|
||||
// should be "desc" and not "asc" because "minus" is added which will reverse order
|
||||
->orderBy(DB::raw("-{$prefix}creditables.order"), 'desc');
|
||||
}
|
||||
|
||||
protected function groupByDepartment(Builder $builder): Collection
|
||||
{
|
||||
$credits = $builder->get()->map(function ($credit) {
|
||||
$credit->pivot = [
|
||||
'id' => $credit->pivot_id,
|
||||
'job' => $credit->pivot_job,
|
||||
'department' => $credit->pivot_department,
|
||||
'character' => $credit->pivot_character,
|
||||
];
|
||||
|
||||
unset(
|
||||
$credit->pivot_id,
|
||||
$credit->pivot_creditable_id,
|
||||
$credit->pivot_creditable_type,
|
||||
$credit->pivot_job,
|
||||
$credit->pivot_department,
|
||||
$credit->pivot_order,
|
||||
$credit->pivot_character,
|
||||
);
|
||||
|
||||
return $credit;
|
||||
});
|
||||
|
||||
return $credits->groupBy('pivot.department');
|
||||
}
|
||||
}
|
||||
79
app/Actions/Videos/CrupdateVideo.php
Executable file
79
app/Actions/Videos/CrupdateVideo.php
Executable 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
61
app/Console/Commands/CleanDemoSite.php
Executable file
61
app/Console/Commands/CleanDemoSite.php
Executable file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Auth\Permissions\Permission;
|
||||
use Common\Auth\Permissions\Traits\SyncsPermissions;
|
||||
use Common\Database\Seeds\DefaultPagesSeeder;
|
||||
use Common\Localizations\Localization;
|
||||
use Common\Pages\CustomPage;
|
||||
use Hash;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanDemoSite extends Command
|
||||
{
|
||||
use SyncsPermissions;
|
||||
|
||||
protected $signature = 'demo:clean';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// reset admin user
|
||||
$this->cleanAdminUser('admin@admin.com');
|
||||
|
||||
// delete localizations
|
||||
Localization::get()->each(function (Localization $localization) {
|
||||
if (strtolower($localization->name) !== 'english') {
|
||||
$localization->delete();
|
||||
}
|
||||
});
|
||||
|
||||
// delete custom pages
|
||||
CustomPage::truncate();
|
||||
|
||||
app(DefaultPagesSeeder::class)->run();
|
||||
}
|
||||
|
||||
private function cleanAdminUser($email): void
|
||||
{
|
||||
$admin = User::where('email', $email)->first();
|
||||
|
||||
if (!$admin) {
|
||||
$admin = User::create([
|
||||
'email' => $email,
|
||||
]);
|
||||
}
|
||||
|
||||
$admin->avatar = null;
|
||||
$admin->username = 'admin';
|
||||
$admin->first_name = 'Demo';
|
||||
$admin->last_name = 'Admin';
|
||||
$admin->password = 'admin';
|
||||
$admin->email_verified_at = now()->subDays(10);
|
||||
$admin->save();
|
||||
|
||||
$adminPermission = app(Permission::class)
|
||||
->where('name', 'admin')
|
||||
->first();
|
||||
$this->syncPermissions($admin, [$adminPermission]);
|
||||
}
|
||||
}
|
||||
269
app/Console/Commands/GenerateDemoDataCommand.php
Executable file
269
app/Console/Commands/GenerateDemoDataCommand.php
Executable file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\Demo\GenerateDemoAnimeVideos;
|
||||
use App\Actions\Demo\GenerateDemoComments;
|
||||
use App\Actions\Demo\GenerateDemoReviews;
|
||||
use App\Actions\Demo\GenerateDemoStreamVideos;
|
||||
use App\Actions\Demo\GenerateDemoUsers;
|
||||
use App\Actions\Demo\GenerateDemoVideoVotes;
|
||||
use App\Models\Channel;
|
||||
use Common\Admin\Appearance\Themes\CssTheme;
|
||||
use Common\Channels\GenerateChannelsFromConfig;
|
||||
use Common\Channels\UpdateAllChannelsContent;
|
||||
use Common\Settings\DotEnvEditor;
|
||||
use Common\Settings\Setting;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GenerateDemoDataCommand extends Command
|
||||
{
|
||||
protected $signature = 'demo:generate {variant} {--truncate}';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
@set_time_limit(0);
|
||||
@ini_set('memory_limit', '200M');
|
||||
|
||||
Artisan::call('down');
|
||||
|
||||
$variant = $this->argument('variant');
|
||||
|
||||
if ($variant === 'database') {
|
||||
$this->databaseVariant();
|
||||
} elseif ($variant === 'streaming') {
|
||||
$this->streamingVariant();
|
||||
} elseif ($variant === 'anime') {
|
||||
$this->animeVariant();
|
||||
} else {
|
||||
$this->error('Invalid variant');
|
||||
}
|
||||
|
||||
$this->info('Demo data generated', true);
|
||||
|
||||
Artisan::call('up');
|
||||
}
|
||||
|
||||
protected function databaseVariant(): void
|
||||
{
|
||||
$this->overridePrimaryMenu([
|
||||
[
|
||||
'id' => 'cVKgbI',
|
||||
'type' => 'channels',
|
||||
'label' => 'Movies',
|
||||
'action' => '/movies',
|
||||
],
|
||||
[
|
||||
'id' => 'nVKg4v',
|
||||
'type' => 'channels',
|
||||
'label' => 'Series',
|
||||
'action' => '/series',
|
||||
],
|
||||
[
|
||||
'id' => 'nVKg1Ix',
|
||||
'type' => 'channels',
|
||||
'label' => 'People',
|
||||
'action' => '/people',
|
||||
],
|
||||
[
|
||||
'id' => 'nVKxdI',
|
||||
'type' => 'channels',
|
||||
'label' => 'News',
|
||||
'action' => '/latest-news',
|
||||
],
|
||||
]);
|
||||
|
||||
$homepageChannel = $this->generateChannels([
|
||||
resource_path('defaults/channels/shared-channels.json'),
|
||||
resource_path('defaults/channels/default-channels.json'),
|
||||
]);
|
||||
settings()->save([
|
||||
'homepage.type' => 'channels',
|
||||
'homepage.value' => $homepageChannel->id,
|
||||
'streaming.video_panel_content' => 'all',
|
||||
'streaming.prefer_full' => false,
|
||||
'streaming.show_video_selector' => false,
|
||||
'streaming.show_header_play' => true,
|
||||
'content.search_provider' => 'tmdb',
|
||||
'content.title_provider' => 'tmdb',
|
||||
'content.people_provider' => 'tmdb',
|
||||
'content.automate_filmography' => true,
|
||||
'title_page.sections' => json_encode([
|
||||
'seasons',
|
||||
'videos',
|
||||
'images',
|
||||
'reviews',
|
||||
'cast',
|
||||
'related',
|
||||
]),
|
||||
]);
|
||||
|
||||
Artisan::call(UpdateAllChannelsContent::class);
|
||||
|
||||
(new GenerateDemoUsers())->execute();
|
||||
(new GenerateDemoVideoVotes())->execute();
|
||||
(new GenerateDemoComments())->execute();
|
||||
(new GenerateDemoReviews())->execute();
|
||||
}
|
||||
|
||||
protected function streamingVariant(): void
|
||||
{
|
||||
if ($this->option('truncate')) {
|
||||
Artisan::call(TruncateTitleData::class);
|
||||
}
|
||||
|
||||
$this->overridePrimaryMenu([
|
||||
[
|
||||
'id' => 'cVKg0I',
|
||||
'type' => 'route',
|
||||
'label' => 'Movies',
|
||||
'action' => '/movies',
|
||||
],
|
||||
[
|
||||
'id' => 'nVKg0v',
|
||||
'type' => 'route',
|
||||
'label' => 'TV shows',
|
||||
'action' => '/series',
|
||||
],
|
||||
[
|
||||
'id' => 'nVKg0Ix',
|
||||
'type' => 'route',
|
||||
'label' => 'Watchlist',
|
||||
'action' => '/watchlist',
|
||||
],
|
||||
]);
|
||||
|
||||
$homepageChannel = $this->generateChannels([
|
||||
resource_path('defaults/channels/shared-channels.json'),
|
||||
resource_path('defaults/channels/streaming-channels.json'),
|
||||
]);
|
||||
|
||||
$darkTheme = CssTheme::where('default_dark', true)->first();
|
||||
|
||||
settings()->save([
|
||||
'themes.default_id' => $darkTheme->id,
|
||||
'homepage.type' => 'landingPage',
|
||||
'homepage.value' => $homepageChannel->id,
|
||||
'streaming.video_panel_content' => 'full',
|
||||
'streaming.prefer_full' => true,
|
||||
'streaming.show_video_selector' => false,
|
||||
'streaming.show_header_play' => true,
|
||||
'content.search_provider' => 'local',
|
||||
'content.title_provider' => 'tmdb',
|
||||
'content.people_provider' => 'tmdb',
|
||||
'content.automate_filmography' => false,
|
||||
'title_page.sections' => json_encode([
|
||||
'seasons',
|
||||
'images',
|
||||
'reviews',
|
||||
'cast',
|
||||
'related',
|
||||
]),
|
||||
]);
|
||||
|
||||
app(DotEnvEditor::class)->write([
|
||||
'scout_driver' => 'meilisearch',
|
||||
]);
|
||||
|
||||
$this->info('Updating channels');
|
||||
Artisan::call(UpdateAllChannelsContent::class);
|
||||
$this->info('Channels updated.');
|
||||
$this->info('Updating seasons');
|
||||
Artisan::call(UpdateSeasonsFromRemote::class);
|
||||
$this->info('Seasons updated.');
|
||||
|
||||
(new GenerateDemoUsers())->execute();
|
||||
(new GenerateDemoStreamVideos())->execute();
|
||||
(new GenerateDemoVideoVotes())->execute();
|
||||
(new GenerateDemoComments())->execute();
|
||||
(new GenerateDemoReviews())->execute();
|
||||
}
|
||||
|
||||
protected function animeVariant(): void
|
||||
{
|
||||
if ($this->option('truncate')) {
|
||||
Artisan::call(TruncateTitleData::class);
|
||||
}
|
||||
|
||||
$this->overridePrimaryMenu([
|
||||
[
|
||||
'id' => 'cVKg0I',
|
||||
'type' => 'route',
|
||||
'label' => 'Movies',
|
||||
'action' => '/movies',
|
||||
],
|
||||
[
|
||||
'id' => 'nVKg0v',
|
||||
'type' => 'route',
|
||||
'label' => 'TV shows',
|
||||
'action' => '/series',
|
||||
],
|
||||
[
|
||||
'id' => 'nVKg0Ix',
|
||||
'type' => 'route',
|
||||
'label' => 'Watchlist',
|
||||
'action' => '/watchlist',
|
||||
],
|
||||
]);
|
||||
|
||||
$homepageChannel = $this->generateChannels([
|
||||
resource_path('defaults/channels/shared-channels.json'),
|
||||
resource_path('defaults/channels/anime-channels.json'),
|
||||
]);
|
||||
|
||||
settings()->save([
|
||||
'homepage.type' => 'channels',
|
||||
'homepage.value' => $homepageChannel->id,
|
||||
'streaming.video_panel_content' => 'full',
|
||||
'streaming.prefer_full' => true,
|
||||
'streaming.show_video_selector' => true,
|
||||
'streaming.show_header_play' => true,
|
||||
'content.search_provider' => 'local',
|
||||
'content.title_provider' => 'tmdb',
|
||||
'content.people_provider' => 'tmdb',
|
||||
'content.automate_filmography' => false,
|
||||
'title_page.sections' => json_encode([
|
||||
'episodes',
|
||||
'images',
|
||||
'reviews',
|
||||
'cast',
|
||||
'related',
|
||||
]),
|
||||
]);
|
||||
|
||||
app(DotEnvEditor::class)->write([
|
||||
'scout_driver' => 'meilisearch',
|
||||
]);
|
||||
|
||||
Artisan::call(UpdateAllChannelsContent::class);
|
||||
|
||||
(new GenerateDemoUsers())->execute();
|
||||
(new GenerateDemoAnimeVideos())->execute();
|
||||
(new GenerateDemoVideoVotes())->execute();
|
||||
(new GenerateDemoComments())->execute('anime');
|
||||
(new GenerateDemoReviews())->execute();
|
||||
}
|
||||
|
||||
protected function overridePrimaryMenu(array $items): void
|
||||
{
|
||||
$menus = Setting::where('name', 'menus')->first()->value;
|
||||
$index = array_search('primary', array_column($menus, 'name'));
|
||||
$menus[$index]['items'] = $items;
|
||||
Setting::where('name', 'menus')->update([
|
||||
'value' => json_encode($menus),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function generateChannels(array $paths): ?Channel
|
||||
{
|
||||
$ids = Channel::where('type', 'channel')->pluck('id');
|
||||
DB::table('channelables')
|
||||
->whereIn('channel_id', $ids)
|
||||
->delete();
|
||||
Channel::whereIn('id', $ids)->delete();
|
||||
|
||||
return (new GenerateChannelsFromConfig())->execute($paths);
|
||||
}
|
||||
}
|
||||
18
app/Console/Commands/GenerateSitemap.php
Executable file
18
app/Console/Commands/GenerateSitemap.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\SitemapGenerator;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class GenerateSitemap extends Command
|
||||
{
|
||||
protected $signature = 'sitemap:generate';
|
||||
|
||||
protected $description = 'Generate sitemaps for all site resources.';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
app(SitemapGenerator::class)->generate();
|
||||
}
|
||||
}
|
||||
103
app/Console/Commands/SyncTmdbValueLists.php
Executable file
103
app/Console/Commands/SyncTmdbValueLists.php
Executable file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Data\Tmdb\TmdbApi;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class SyncTmdbValueLists extends Command
|
||||
{
|
||||
protected $signature = 'tmdb:syncValueLists';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->syncGenres();
|
||||
$this->syncCountries();
|
||||
$this->syncDepartments();
|
||||
$this->syncLanguages();
|
||||
$this->syncKeywords();
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function syncKeywords(): void
|
||||
{
|
||||
$downloadUrl =
|
||||
'http://files.tmdb.org/p/exports/keyword_ids_07_08_2023.json.gz';
|
||||
$content = gzdecode(file_get_contents($downloadUrl));
|
||||
$content = str_replace('"}', '"},', $content);
|
||||
$content = rtrim(trim($content), ',');
|
||||
file_put_contents(resource_path('lists/tmdb-keywords.json'), '[' . $content . ']');
|
||||
}
|
||||
|
||||
protected function syncGenres(): void
|
||||
{
|
||||
$movieGenres = Http::get(TmdbApi::TMDB_BASE . 'genre/movie/list', [
|
||||
'api_key' => config('services.tmdb.key'),
|
||||
'language' => 'en-US',
|
||||
])->json()['genres'];
|
||||
|
||||
$tvGenres = Http::get(TmdbApi::TMDB_BASE . 'genre/tv/list', [
|
||||
'api_key' => config('services.tmdb.key'),
|
||||
'language' => 'en-US',
|
||||
])->json()['genres'];
|
||||
|
||||
$mergedGenres = collect($movieGenres)
|
||||
->merge($tvGenres)
|
||||
->unique('id')
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
file_put_contents(
|
||||
resource_path('lists/tmdb-genres.json'),
|
||||
json_encode($mergedGenres),
|
||||
);
|
||||
}
|
||||
|
||||
private function syncLanguages(): void
|
||||
{
|
||||
$languages = Http::get(TmdbApi::TMDB_BASE . 'configuration/languages', [
|
||||
'api_key' => config('services.tmdb.key'),
|
||||
])->json();
|
||||
|
||||
$languages = array_map(fn($language) => [
|
||||
'code' => $language['iso_639_1'],
|
||||
'name' => $language['english_name'],
|
||||
], $languages);
|
||||
|
||||
file_put_contents(
|
||||
resource_path('lists/tmdb-languages.json'),
|
||||
json_encode($languages),
|
||||
);
|
||||
}
|
||||
|
||||
protected function syncDepartments(): void
|
||||
{
|
||||
$departments = Http::get(TmdbApi::TMDB_BASE . 'configuration/jobs', [
|
||||
'api_key' => config('services.tmdb.key'),
|
||||
])->json();
|
||||
|
||||
file_put_contents(
|
||||
resource_path('lists/tmdb-departments.json'),
|
||||
json_encode($departments),
|
||||
);
|
||||
}
|
||||
|
||||
protected function syncCountries(): void
|
||||
{
|
||||
$countries = Http::get(TmdbApi::TMDB_BASE . 'configuration/countries', [
|
||||
'api_key' => config('services.tmdb.key'),
|
||||
])->json();
|
||||
|
||||
$countries = array_map(fn($country) => [
|
||||
'code' => $country['iso_3166_1'],
|
||||
'name' => $country['english_name'],
|
||||
], $countries);
|
||||
|
||||
file_put_contents(
|
||||
resource_path('lists/tmdb-countries.json'),
|
||||
json_encode($countries),
|
||||
);
|
||||
}
|
||||
}
|
||||
45
app/Console/Commands/TruncateTitleData.php
Executable file
45
app/Console/Commands/TruncateTitleData.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use DB;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class TruncateTitleData extends Command
|
||||
{
|
||||
protected $signature = 'titles:truncate';
|
||||
|
||||
protected $description = 'Truncate all title related database tables.';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
DB::table('channelables')->truncate();
|
||||
DB::table('channels')->truncate();
|
||||
DB::table('comment_reports')->truncate();
|
||||
DB::table('comment_votes')->truncate();
|
||||
DB::table('comments')->truncate();
|
||||
DB::table('country_title')->truncate();
|
||||
DB::table('creditables')->truncate();
|
||||
DB::table('episodes')->truncate();
|
||||
DB::table('genre_title')->truncate();
|
||||
DB::table('genres')->truncate();
|
||||
DB::table('images')->truncate();
|
||||
DB::table('keyword_title')->truncate();
|
||||
DB::table('keywords')->truncate();
|
||||
DB::table('production_countries')->truncate();
|
||||
DB::table('review_feedback')->truncate();
|
||||
DB::table('review_reports')->truncate();
|
||||
DB::table('reviews')->truncate();
|
||||
DB::table('seasons')->truncate();
|
||||
DB::table('tags')->truncate();
|
||||
DB::table('taggables')->truncate();
|
||||
DB::table('people')->truncate();
|
||||
DB::table('titles')->truncate();
|
||||
DB::table('videos')->truncate();
|
||||
DB::table('video_captions')->truncate();
|
||||
DB::table('video_reports')->truncate();
|
||||
DB::table('video_plays')->truncate();
|
||||
DB::table('video_votes')->truncate();
|
||||
DB::table('news_article_models')->truncate();
|
||||
}
|
||||
}
|
||||
20
app/Console/Commands/UpdateNewsFromRemote.php
Executable file
20
app/Console/Commands/UpdateNewsFromRemote.php
Executable file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\News\ImportNewsFromRemoteProvider;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpdateNewsFromRemote extends Command
|
||||
{
|
||||
protected $signature = 'news:update';
|
||||
|
||||
protected $description = 'Update news from currently selected 3rd party site.';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
app(ImportNewsFromRemoteProvider::class)->execute();
|
||||
|
||||
$this->info('News updated.');
|
||||
}
|
||||
}
|
||||
27
app/Console/Commands/UpdateSeasonsFromRemote.php
Executable file
27
app/Console/Commands/UpdateSeasonsFromRemote.php
Executable file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Season;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpdateSeasonsFromRemote extends Command
|
||||
{
|
||||
protected $signature = 'seasons:update';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$seasons = Season::orderBy('updated_at', 'asc')
|
||||
->with('title')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
$this->withProgressBar($seasons, function (Season $season) {
|
||||
if ($season->title) {
|
||||
$season->maybeUpdateFromExternal($season->title);
|
||||
}
|
||||
});
|
||||
|
||||
$this->info('Seasons updated');
|
||||
}
|
||||
}
|
||||
45
app/Console/Kernel.php
Executable file
45
app/Console/Kernel.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Console\Commands\CleanDemoSite;
|
||||
use App\Console\Commands\UpdateNewsFromRemote;
|
||||
use App\Console\Commands\UpdateSeasonsFromRemote;
|
||||
use Common\Channels\UpdateAllChannelsContent;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
protected $commands = [UpdateAllChannelsContent::class];
|
||||
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
if (settings('news.auto_update')) {
|
||||
$schedule->command(UpdateNewsFromRemote::class)->daily();
|
||||
}
|
||||
|
||||
if (
|
||||
config('services.tmdb.key') &&
|
||||
(settings('content.force_season_update') ||
|
||||
settings('content.title_provider') === 'tmdb')
|
||||
) {
|
||||
$schedule
|
||||
->command(UpdateSeasonsFromRemote::class)
|
||||
->everyFourHours();
|
||||
}
|
||||
|
||||
if (config('common.site.demo')) {
|
||||
$schedule->command(CleanDemoSite::class)->daily();
|
||||
}
|
||||
|
||||
$schedule->command(UpdateAllChannelsContent::class)->daily();
|
||||
}
|
||||
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
10
app/Exceptions/Handler.php
Executable file
10
app/Exceptions/Handler.php
Executable file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Common\Core\Exceptions\BaseExceptionHandler;
|
||||
|
||||
class Handler extends BaseExceptionHandler
|
||||
{
|
||||
//
|
||||
}
|
||||
65
app/Http/Controllers/AdminTitleTagsController.php
Executable file
65
app/Http/Controllers/AdminTitleTagsController.php
Executable file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
|
||||
class AdminTitleTagsController extends BaseController
|
||||
{
|
||||
public function index(string $tagType)
|
||||
{
|
||||
$this->authorize('index', Title::class);
|
||||
|
||||
$builder = app(modelTypeToNamespace($tagType))->newQuery();
|
||||
|
||||
$dataSource = new Datasource($builder, request()->all());
|
||||
|
||||
$pagination = $dataSource->paginate();
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function store(string $type) {
|
||||
$this->authorize('store', Title::class);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'name' => 'required|string',
|
||||
'display_name' => 'string',
|
||||
]);
|
||||
|
||||
$tag = app(modelTypeToNamespace($type))->create($data);
|
||||
|
||||
return $this->success(['tag' => $tag]);
|
||||
}
|
||||
|
||||
public function update(string $type, int $tagId) {
|
||||
$this->authorize('update', Title::class);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'name' => 'string',
|
||||
'display_name' => 'string',
|
||||
]);
|
||||
|
||||
$tag = app(modelTypeToNamespace($type))->findOrFail($tagId);
|
||||
|
||||
$tag->update($data);
|
||||
|
||||
return $this->success(['tag' => $tag]);
|
||||
}
|
||||
|
||||
public function destroy($type, string $ids)
|
||||
{
|
||||
$tagIds = explode(',', $ids);
|
||||
$this->authorize('destroy', Title::class);
|
||||
|
||||
foreach ($tagIds as $tagId) {
|
||||
$tag = app(modelTypeToNamespace($type))->findOrFail($tagId);
|
||||
$tag->titles()->detach();
|
||||
$tag->delete();
|
||||
}
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
297
app/Http/Controllers/ApiTmdbController.php
Executable file
297
app/Http/Controllers/ApiTmdbController.php
Executable file
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
use App\Models\Genre;
|
||||
use App\Models\ProductionCountry;
|
||||
use App\Models\Title;
|
||||
use App\Models\Image;
|
||||
use App\Models\Keyword;
|
||||
use App\Models\Person;
|
||||
use App\Models\Video;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ApiTmdbController extends BaseController
|
||||
{
|
||||
|
||||
protected $baseUrl = 'https://api.themoviedb.org/3';
|
||||
protected $token = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJjYTJiNzZmNDkyZWQxNGFmMmU2Njk4N2E2YmRjZDY0ZiIsIm5iZiI6MTc0OTQ2MzY4NC41NjksInN1YiI6IjY4NDZiMjg0NDQ5MDEyOGMxMzNmYzk1NiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.PXMuq0fxXaC2kR1qrd-LDeezz0HAhL8o3-4QGaik0D4";
|
||||
|
||||
private array $sizes = [
|
||||
'original' => null,
|
||||
'large' => 500,
|
||||
'medium' => 300,
|
||||
'small' => 92,
|
||||
];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$movieId = request('movieId');
|
||||
//$movieId = 1284120;
|
||||
|
||||
/****************************************** */
|
||||
$responseMovie = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
'Accept' => 'application/json',
|
||||
])->get("$this->baseUrl/movie/$movieId", [
|
||||
'language' => 'fr-FR'
|
||||
]);
|
||||
|
||||
$movie = $responseMovie->successful() ? $responseMovie->json() : [];
|
||||
|
||||
|
||||
$title = Title::create([
|
||||
'imdb_id' => $movie['id'],
|
||||
'backdrop' => "https://image.tmdb.org/t/p/original".$movie['backdrop_path'],
|
||||
'poster' => "https://image.tmdb.org/t/p/original".$movie['poster_path'],
|
||||
'name' => $movie['title'],
|
||||
'original_title' => $movie['original_title'],
|
||||
'language' => $movie['original_language'],
|
||||
'is_series' => false,
|
||||
'release_date' => $movie['release_date'],
|
||||
'tagline' => $movie['tagline'],
|
||||
'description' => $movie['overview'],
|
||||
'runtime' => $movie['runtime'],
|
||||
//////////////////'certification' => $movie[''],
|
||||
'budget' => $movie['budget'],
|
||||
'revenue' => $movie['revenue'],
|
||||
'popularity' => $movie['popularity'],
|
||||
'language' => $movie['original_language'],
|
||||
'adult' => $movie['adult'],
|
||||
'type' => 'movie',
|
||||
'tmdb_vote_average' => $movie['vote_average']
|
||||
]);
|
||||
|
||||
|
||||
$genres = $countrys = [];
|
||||
|
||||
foreach($movie['production_countries'] as $country)
|
||||
{
|
||||
$productionCountry = ProductionCountry::updateOrCreate(
|
||||
['name' => $country['iso_3166_1']],
|
||||
['name' => $country['iso_3166_1'], 'display_name' => $country['name']
|
||||
]);
|
||||
$countrys[] = $productionCountry->id;
|
||||
}
|
||||
|
||||
foreach($movie['genres'] as $genre)
|
||||
{
|
||||
$genre = Genre::updateOrCreate(
|
||||
['name' => $genre['name']],
|
||||
[
|
||||
'name' => $genre['name'],
|
||||
'display_name' => $genre['name']
|
||||
]);
|
||||
$genres[] = $genre->id;
|
||||
}
|
||||
|
||||
$title->productionCountries()->attach($countrys);
|
||||
$title->genres()->attach($genres);
|
||||
|
||||
/*********************************************** */
|
||||
$responseMovieKeyword = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
'Accept' => 'application/json',
|
||||
])->get("$this->baseUrl/movie/$movieId/keywords", [
|
||||
'language' => 'fr-FR'
|
||||
]);
|
||||
|
||||
$movieKeywords = $responseMovieKeyword->successful() ? $responseMovieKeyword->json() : [];
|
||||
|
||||
$keywords = [];
|
||||
foreach($movieKeywords['keywords'] as $valKey)
|
||||
{
|
||||
$keyword = Keyword::updateOrCreate(
|
||||
['name' => $valKey['name']],
|
||||
[
|
||||
'name' => $valKey['name'],
|
||||
'display_name' => $valKey['name']
|
||||
]);
|
||||
$keywords[] = $keyword->id;
|
||||
}
|
||||
$title->keywords()->attach($keywords);
|
||||
|
||||
/************************************************/
|
||||
|
||||
$responseMovieCredits = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
'Accept' => 'application/json',
|
||||
])->get("$this->baseUrl/movie/$movieId/credits", [
|
||||
'language' => 'fr-FR'
|
||||
]);
|
||||
|
||||
$movieCredits = $responseMovieCredits->successful() ? $responseMovieCredits->json() : [];
|
||||
|
||||
|
||||
foreach($movieCredits['cast'] as $cast)
|
||||
{
|
||||
$person_id = $this->getPeopleByTMDBid($cast['id']);
|
||||
$title->createCredit([
|
||||
'person_id'=>$person_id,
|
||||
'department'=> "actors",
|
||||
'job'=>"actor",
|
||||
'order'=>$cast['order'],
|
||||
'character'=>$cast['character'],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
foreach($movieCredits['crew'] as $cast)
|
||||
{
|
||||
$person_id = $this->getPeopleByTMDBid($cast['id']);
|
||||
$title->createCredit([
|
||||
'person_id'=>$person_id,
|
||||
'department'=> $cast['department'],
|
||||
'job'=>$cast['job'],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
/********************* Videos *******************************/
|
||||
|
||||
$responseMovieVideos = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
'Accept' => 'application/json',
|
||||
])->get("$this->baseUrl/movie/$movieId/videos", [
|
||||
'language' => 'fr-FR'
|
||||
]);
|
||||
|
||||
$MovieVideos = $responseMovieVideos->successful() ? $responseMovieVideos->json() : [];
|
||||
foreach($MovieVideos['results'] as $keyV =>$video)
|
||||
{
|
||||
if( ($video['site'] == "YouTube") && ($video['type'] == "Trailer") )
|
||||
{
|
||||
Video::create([
|
||||
'name'=>$video['name'],
|
||||
'src'=>"https://youtube.com/embed/".$video['key'],
|
||||
'type'=>'embed',
|
||||
'quality'=> ($video['size']==1080)? "1080p" : null,
|
||||
'title_id'=>$title->id,
|
||||
'origin'=>"local",
|
||||
'downvotes'=>0,
|
||||
'upvotes'=>0,
|
||||
'approved'=>1,
|
||||
'order'=>$keyV,
|
||||
'user_id'=>auth()->id(),
|
||||
'language'=>$video['iso_639_1'],
|
||||
'category'=>'trailer',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/***********************Images***************************/
|
||||
|
||||
$responseMovieImages = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
'Accept' => 'application/json',
|
||||
])->get("$this->baseUrl/movie/$movieId/images", []);
|
||||
|
||||
$movieImages = $responseMovieImages->successful() ? $responseMovieImages->json() : [];
|
||||
for($i=0; $i<=5; $i++)
|
||||
{
|
||||
$nameFileImage = isset($movieImages['backdrops'][$i]['file_path'])? $movieImages['backdrops'][$i]['file_path'] : null;
|
||||
if($nameFileImage)
|
||||
{
|
||||
$url = $this->saveImage("https://image.tmdb.org/t/p/original$nameFileImage");
|
||||
|
||||
Image::where('model_type', Title::MODEL_TYPE)
|
||||
->where('model_id', $title->id)
|
||||
->increment('order');
|
||||
|
||||
Image::create([
|
||||
'url' => $url,
|
||||
'type' => 'backdrop',
|
||||
'source' => 'local',
|
||||
'model_type' => Title::MODEL_TYPE,
|
||||
'model_id' => $title->id,
|
||||
'order' => 0
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getPeopleByTMDBid($tmdb_id)
|
||||
{
|
||||
$person = DB::table('people')->where('tmdb_id', $tmdb_id)->first();
|
||||
|
||||
if(!$person)
|
||||
{
|
||||
$responsePeople = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
'Accept' => 'application/json',
|
||||
])->get("$this->baseUrl/person/$tmdb_id", [
|
||||
'language' => 'fr-FR'
|
||||
]);
|
||||
|
||||
$people = $responsePeople->successful() ? $responsePeople->json() : [];
|
||||
$person = Person::create([
|
||||
'tmdb_id' => $people['id'],
|
||||
'name' => $people['name'],
|
||||
'description'=>$people['biography'],
|
||||
'gender'=>$people['gender'],
|
||||
'birth_date'=>$people['birthday'],
|
||||
'birth_place'=>$people['place_of_birth'],
|
||||
'poster'=>"https://image.tmdb.org/t/p/w500".$people['profile_path'],
|
||||
'imdb_id'=>$people['imdb_id'],
|
||||
'known_for'=>$people['known_for_department'],
|
||||
'popularity'=>$people['popularity'],
|
||||
'death_date'=>$people['deathday'],
|
||||
'adult'=>$people['adult'],
|
||||
]);
|
||||
}
|
||||
return $person->id ;
|
||||
}
|
||||
|
||||
|
||||
public function saveImage(string $urlImage): string
|
||||
{
|
||||
$hash = Str::random(30);
|
||||
$manager = new ImageManager(new Driver());
|
||||
|
||||
// Télécharger l'image depuis l'URL
|
||||
$imageContent = file_get_contents($urlImage);
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'img');
|
||||
file_put_contents($tempFile, $imageContent);
|
||||
|
||||
try {
|
||||
$img = $manager->read($tempFile);
|
||||
|
||||
// Déterminer l'extension à partir de l'URL ou utiliser jpeg par défaut
|
||||
$pathInfo = pathinfo(parse_url($urlImage, PHP_URL_PATH));
|
||||
$extension = $pathInfo['extension'] ?? 'jpeg';
|
||||
$extension = in_array(strtolower($extension), ['jpg', 'jpeg', 'png']) ? $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);
|
||||
} finally {
|
||||
// Nettoyer le fichier temporaire
|
||||
if (file_exists($tempFile)) {
|
||||
unlink($tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
app/Http/Controllers/AppHomeController.php
Executable file
38
app/Http/Controllers/AppHomeController.php
Executable file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Title;
|
||||
use Auth;
|
||||
use Common\Billing\Models\Product;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class AppHomeController extends BaseController
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
if (
|
||||
settings('homepage.type') === 'channels' ||
|
||||
(Auth::check() && settings('homepage.type') === 'landingPage')
|
||||
) {
|
||||
return app(FallbackRouteController::class)->renderChannel(
|
||||
settings('homepage.value'),
|
||||
);
|
||||
} else {
|
||||
return $this->renderClientOrApi([
|
||||
'pageName' => 'landing-page',
|
||||
'data' => [
|
||||
'loader' => 'landingPage',
|
||||
'products' => Product::with(['permissions', 'prices'])
|
||||
->limit(15)
|
||||
->orderBy('position', 'asc')
|
||||
->get(),
|
||||
'trendingTitles' => Title::orderBy('popularity', 'desc')
|
||||
->compact()
|
||||
->limit(6)
|
||||
->get(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
app/Http/Controllers/ChannelItemController.php
Executable file
50
app/Http/Controllers/ChannelItemController.php
Executable file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ChannelItemController extends BaseController
|
||||
{
|
||||
public function add(Channel $channel)
|
||||
{
|
||||
$this->authorize('update', $channel);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'itemId' => 'required|integer',
|
||||
'itemType' => 'required|string',
|
||||
]);
|
||||
|
||||
$relationName = Str::plural($data['itemType']);
|
||||
|
||||
$channel->$relationName()->sync(
|
||||
[
|
||||
$data['itemId'] => [
|
||||
'order' => $channel->$relationName()->count() + 1,
|
||||
],
|
||||
],
|
||||
false,
|
||||
);
|
||||
$channel->touch();
|
||||
|
||||
return $this->success(['channel' => $channel]);
|
||||
}
|
||||
|
||||
public function remove(Channel $channel)
|
||||
{
|
||||
$this->authorize('update', $channel);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'itemId' => 'required|integer',
|
||||
'itemType' => 'required|string',
|
||||
]);
|
||||
|
||||
$relationName = Str::plural($data['itemType']);
|
||||
$channel->$relationName()->detach($data['itemId']);
|
||||
$channel->touch();
|
||||
|
||||
return $this->success(['channel' => $channel]);
|
||||
}
|
||||
}
|
||||
104
app/Http/Controllers/EpisodeController.php
Executable file
104
app/Http/Controllers/EpisodeController.php
Executable file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Loaders\EpisodeLoader;
|
||||
use App\Models\Episode;
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class EpisodeController extends BaseController
|
||||
{
|
||||
public function show()
|
||||
{
|
||||
$data = (new EpisodeLoader())->loadData(request('loader'));
|
||||
|
||||
$this->authorize('show', $data['title']);
|
||||
|
||||
return $this->renderClientOrApi([
|
||||
'data' => $data,
|
||||
'pageName' => 'episode-page',
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Title $title, int $seasonNumber, int $episodeNumber)
|
||||
{
|
||||
$this->authorize('update', $title);
|
||||
|
||||
$episode = $title
|
||||
->episodes()
|
||||
->where('season_number', $seasonNumber)
|
||||
->where('episode_number', $episodeNumber)
|
||||
->firstOrFail();
|
||||
|
||||
$this->validate(request(), [
|
||||
'episode_number' => [
|
||||
'integer',
|
||||
Rule::unique('episodes')
|
||||
->ignore($episode->episode_number, 'episode_number')
|
||||
->where(function (Builder $query) use ($episode) {
|
||||
$query
|
||||
->where('season_number', $episode->season_number)
|
||||
->where('title_id', $episode->title_id);
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
$episode->fill(request()->all())->save();
|
||||
|
||||
return $this->success(['episode' => $episode]);
|
||||
}
|
||||
|
||||
public function store(Title $title, int $seasonNumber)
|
||||
{
|
||||
$this->authorize('update', $title);
|
||||
|
||||
$season = $title->findSeason($seasonNumber)->loadCount('episodes');
|
||||
|
||||
$this->validate(request(), [
|
||||
'episode_number' => [
|
||||
'integer',
|
||||
Rule::unique('episodes')->where(function (Builder $query) use (
|
||||
$season,
|
||||
) {
|
||||
$query
|
||||
->where('season_number', $season->number)
|
||||
->where('title_id', $season->title_id);
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
$epNum = request('episode_number');
|
||||
if (!$epNum) {
|
||||
$epNum =
|
||||
$season
|
||||
->episodes()
|
||||
->orderBy('episode_number', 'desc')
|
||||
->value('episode_number') + 1;
|
||||
}
|
||||
|
||||
$episode = Episode::create(
|
||||
array_merge(request()->all(), [
|
||||
'season_number' => $season->number,
|
||||
'episode_number' => $epNum,
|
||||
'season_id' => $season->id,
|
||||
'title_id' => $season->title_id,
|
||||
]),
|
||||
);
|
||||
|
||||
return $this->success(['episode' => $episode]);
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->authorize('destroy', Title::class);
|
||||
|
||||
$episode = Episode::findOrFail($id);
|
||||
$episode->credits()->detach();
|
||||
$episode->delete();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
62
app/Http/Controllers/FallbackRouteController.php
Executable file
62
app/Http/Controllers/FallbackRouteController.php
Executable file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Common\Channels\ChannelController;
|
||||
use Common\Core\Controllers\HomeController;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
class FallbackRouteController
|
||||
{
|
||||
static array $defaultRoutes = ['lists', 'titles', 'search', 'news', 'user'];
|
||||
|
||||
public function __invoke(string $path)
|
||||
{
|
||||
$parts = explode('/', $path);
|
||||
|
||||
if (
|
||||
count($parts) > 2 ||
|
||||
count($parts) < 1 ||
|
||||
(count($parts) === 1 && in_array($parts[0], self::$defaultRoutes))
|
||||
) {
|
||||
return $this->renderClient();
|
||||
}
|
||||
|
||||
// first try to match a channel, if none is found, fallback to rendering client side app
|
||||
try {
|
||||
if ($parts[0] === 'lists' && isset($parts[1])) {
|
||||
request()->merge(['channelType' => 'list']);
|
||||
$parts[0] = $parts[1];
|
||||
$parts[1] = null;
|
||||
}
|
||||
|
||||
if ($parts[0] === 'channel' && isset($parts[1])) {
|
||||
$parts[0] = $parts[1];
|
||||
$parts[1] = null;
|
||||
}
|
||||
|
||||
$slugOrId = $parts[0];
|
||||
$restriction = $parts[1] ?? null;
|
||||
return $this->renderChannel($slugOrId, $restriction);
|
||||
} catch (ModelNotFoundException) {
|
||||
return $this->renderClient();
|
||||
}
|
||||
}
|
||||
|
||||
public function renderChannel(string $slugOrId, ?string $restriction = null)
|
||||
{
|
||||
$channel = app(Channel::class)->resolveRouteBinding($slugOrId);
|
||||
if ($restriction) {
|
||||
request()->merge(['restriction' => $restriction]);
|
||||
}
|
||||
return app(ChannelController::class)->show($channel);
|
||||
}
|
||||
|
||||
public function renderClient()
|
||||
{
|
||||
// no need to prerender channels here, use base HomeController
|
||||
request()->route()->action['uses'] = HomeController::class . '@show';
|
||||
return app(HomeController::class)->show();
|
||||
}
|
||||
}
|
||||
43
app/Http/Controllers/ImageOrderController.php
Executable file
43
app/Http/Controllers/ImageOrderController.php
Executable file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
use DB;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ImageOrderController extends BaseController
|
||||
{
|
||||
public function __construct(private Title $title, private Request $request)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $titleId
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function changeOrder($titleId) {
|
||||
|
||||
$title = $this->title->findOrFail($titleId);
|
||||
|
||||
$this->authorize('update', $title);
|
||||
|
||||
$this->validate($this->request, [
|
||||
'ids' => 'array|min:1',
|
||||
'ids.*' => 'integer'
|
||||
]);
|
||||
|
||||
$queryPart = '';
|
||||
foreach($this->request->get('ids') as $order => $id) {
|
||||
$queryPart .= " when id=$id then $order";
|
||||
}
|
||||
|
||||
DB::table('images')
|
||||
->whereIn('id', $this->request->get('ids'))
|
||||
->update(['order' => DB::raw("(case $queryPart end)")]);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
67
app/Http/Controllers/ImagesController.php
Executable file
67
app/Http/Controllers/ImagesController.php
Executable file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Titles\StoreMediaImageOnDisk;
|
||||
use App\Models\Image;
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
use Storage;
|
||||
|
||||
class ImagesController extends BaseController
|
||||
{
|
||||
public function store()
|
||||
{
|
||||
$titleId = request('titleId');
|
||||
$model = app(Title::class)->findOrFail($titleId);
|
||||
|
||||
$this->authorize('store', $model);
|
||||
|
||||
$this->validate(request(), [
|
||||
'file' => 'required|image|max:10240',
|
||||
'titleId' => 'required|integer',
|
||||
]);
|
||||
|
||||
$url = app(StoreMediaImageOnDisk::class)->execute(
|
||||
request()->file('file'),
|
||||
);
|
||||
|
||||
// put new image at the start of the list when sorted by "order"
|
||||
Image::where('model_type', Title::MODEL_TYPE)
|
||||
->where('model_id', $titleId)
|
||||
->increment('order');
|
||||
|
||||
$image = Image::create([
|
||||
'url' => $url,
|
||||
'type' => 'backdrop',
|
||||
'source' => 'local',
|
||||
'model_type' => Title::MODEL_TYPE,
|
||||
'model_id' => $titleId,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
return $this->success(['image' => $image]);
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$img = Image::findOrFail($id);
|
||||
$model = app(modelTypeToNamespace($img->model_type))->findOrFail(
|
||||
$img->model_id,
|
||||
);
|
||||
|
||||
$this->authorize('destroy', $model);
|
||||
|
||||
if ($img->source === 'local') {
|
||||
// storage/media-images/backdrops/kw4q4eg5g8q4eq6/original.jpg
|
||||
$dir = str_replace('storage/', '', dirname($img->url));
|
||||
if (Storage::disk('public')->exists($dir)) {
|
||||
Storage::disk('public')->deleteDirectory($dir);
|
||||
}
|
||||
}
|
||||
|
||||
$img->delete();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
122
app/Http/Controllers/ImportMediaController.php
Executable file
122
app/Http/Controllers/ImportMediaController.php
Executable file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Person;
|
||||
use App\Models\Title;
|
||||
use App\Services\Data\Tmdb\TmdbApi;
|
||||
use Carbon\Carbon;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class ImportMediaController extends BaseController
|
||||
{
|
||||
public function importMediaItem()
|
||||
{
|
||||
$this->authorize('store', Title::class);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'media_type' => 'required|string',
|
||||
'tmdb_id' => 'required|integer',
|
||||
]);
|
||||
|
||||
if ($data['media_type'] === Person::MODEL_TYPE) {
|
||||
$mediaItem = Person::withoutGlobalScope('adult')
|
||||
->firstOrCreate([
|
||||
'tmdb_id' => $data['tmdb_id'],
|
||||
])
|
||||
->maybeUpdateFromExternal([
|
||||
'forceAutomation' => true,
|
||||
'ignoreLastUpdate' => true,
|
||||
]);
|
||||
} else {
|
||||
$mediaItem = Title::withoutGlobalScope('adult')
|
||||
->firstOrCreate([
|
||||
'tmdb_id' => $data['tmdb_id'],
|
||||
'is_series' => $data['media_type'] === Title::SERIES_TYPE,
|
||||
])
|
||||
->maybeUpdateFromExternal([
|
||||
'forceAutomation' => true,
|
||||
'updateLast3Seasons' => true,
|
||||
'ignoreLastUpdate' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$mediaItem) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return ['mediaItem' => $mediaItem];
|
||||
}
|
||||
|
||||
public function importViaBrowse()
|
||||
{
|
||||
$this->authorize('store', Title::class);
|
||||
|
||||
if (!config('services.tmdb.key')) {
|
||||
abort(
|
||||
403,
|
||||
'Enter your Themoviedb API key in settings page before importing titles.',
|
||||
);
|
||||
}
|
||||
|
||||
@set_time_limit(0);
|
||||
@ini_set('memory_limit', '200M');
|
||||
|
||||
$tmdbParams = [
|
||||
'with_release_type' => '2|3',
|
||||
];
|
||||
|
||||
if (request('country')) {
|
||||
$tmdbParams['with_origin_country'] = strtolower(request('country'));
|
||||
}
|
||||
if (request('language')) {
|
||||
$tmdbParams['with_original_language'] = request('language');
|
||||
}
|
||||
if (request('min_rating')) {
|
||||
$tmdbParams['vote_average.gte'] = request('min_rating');
|
||||
}
|
||||
if (request('max_rating')) {
|
||||
$tmdbParams['vote_average.lte'] = request('max_rating');
|
||||
}
|
||||
if (request('genres')) {
|
||||
$tmdbParams['with_genres'] = request('genres');
|
||||
}
|
||||
if (request('keywords')) {
|
||||
$tmdbParams['with_keywords'] = request('keywords');
|
||||
}
|
||||
if (request('start_date') && request('end_date')) {
|
||||
$tmdbParams['release_date.gte'] = Carbon::parse(
|
||||
request('start_date'),
|
||||
)->format('Y-m-d');
|
||||
$tmdbParams['release_date.lte'] = Carbon::parse(
|
||||
request('end_date'),
|
||||
)->format('Y-m-d');
|
||||
}
|
||||
|
||||
$response = app(TmdbApi::class)->browse(
|
||||
request('current_page', 1),
|
||||
request('type', 'movie'),
|
||||
$tmdbParams,
|
||||
);
|
||||
|
||||
$titles = $response['results']
|
||||
->map(function ($result) {
|
||||
return Title::withoutGlobalScope('adult')
|
||||
->firstOrCreate([
|
||||
'tmdb_id' => $result['tmdb_id'],
|
||||
'is_series' => $result['is_series'],
|
||||
])
|
||||
->maybeUpdateFromExternal([
|
||||
'forceAutomation' => true,
|
||||
]);
|
||||
})
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
return [
|
||||
'titles' => $titles,
|
||||
'total_pages' => $response['total_pages'],
|
||||
'current_page' => request('current_page', 1),
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Controllers/InsightsReportController.php
Executable file
24
app/Http/Controllers/InsightsReportController.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Plays\BuildPlaysReport;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class InsightsReportController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// will authorize based on specified model in "BuildInsightsReport"
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
$report = app(BuildPlaysReport::class)->execute(
|
||||
request()->all(),
|
||||
);
|
||||
|
||||
return $this->success(['report' => $report]);
|
||||
}
|
||||
}
|
||||
19
app/Http/Controllers/ListsController.php
Executable file
19
app/Http/Controllers/ListsController.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Lists\ListsLoader;
|
||||
use App\Models\Channel;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class ListsController extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('index', [Channel::class, 'list']);
|
||||
|
||||
$pagination = (new ListsLoader())->allLists(request()->all());
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
}
|
||||
127
app/Http/Controllers/NewsController.php
Executable file
127
app/Http/Controllers/NewsController.php
Executable file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\News\ImportNewsFromRemoteProvider;
|
||||
use App\Models\NewsArticle;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class NewsController extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('show', NewsArticle::class);
|
||||
|
||||
$paginator = new Datasource(NewsArticle::query(), request()->all());
|
||||
|
||||
$pagination = $paginator->paginate();
|
||||
|
||||
if (request('stripHtml') || request('truncateBody')) {
|
||||
$pagination
|
||||
->map(function (NewsArticle $article) {
|
||||
if (request('stripHtml')) {
|
||||
// remove html tags
|
||||
$article->body = strip_tags($article->body);
|
||||
|
||||
// remove last "...see full article"
|
||||
$parts = explode('...', $article->body);
|
||||
if (
|
||||
count($parts) > 1 &&
|
||||
Str::contains(last($parts), 'See full article')
|
||||
) {
|
||||
array_pop($parts);
|
||||
}
|
||||
$article->body = implode('', $parts);
|
||||
}
|
||||
|
||||
if ($newLength = (int) request('truncateBody')) {
|
||||
$article->body = Str::limit($article->body, $newLength);
|
||||
}
|
||||
|
||||
return $article;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function show($slugOrId)
|
||||
{
|
||||
$article = NewsArticle::where('id', $slugOrId)
|
||||
->orWhere('slug', $slugOrId)
|
||||
->firstOrFail();
|
||||
|
||||
$this->authorize('show', $article);
|
||||
|
||||
$data = [
|
||||
'article' => $article,
|
||||
'related' => NewsArticle::compact()
|
||||
->where('id', '!=', $article->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(10)
|
||||
->get(),
|
||||
'loader' => 'newsArticlePage',
|
||||
];
|
||||
|
||||
return $this->renderClientOrApi([
|
||||
'pageName' => 'news-article-page',
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$article = NewsArticle::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $article);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'title' => 'min:5|max:250',
|
||||
'body' => 'min:5',
|
||||
'image' => 'string',
|
||||
'slug' => 'string',
|
||||
]);
|
||||
|
||||
$article->fill($data)->save();
|
||||
|
||||
return $this->success(['article' => $article]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$this->authorize('store', NewsArticle::class);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'title' => 'required|min:5|max:250',
|
||||
'body' => 'required|min:5',
|
||||
'image' => 'string',
|
||||
'slug' => 'required|string',
|
||||
]);
|
||||
|
||||
$article = NewsArticle::create($data);
|
||||
|
||||
return $this->success(['article' => $article]);
|
||||
}
|
||||
|
||||
public function destroy(string $ids)
|
||||
{
|
||||
$ids = explode(',', $ids);
|
||||
$this->authorize('destroy', NewsArticle::class);
|
||||
|
||||
NewsArticle::whereIn('id', $ids)->delete();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function importFromRemoteProvider()
|
||||
{
|
||||
$this->authorize('store', NewsArticle::class);
|
||||
|
||||
app(ImportNewsFromRemoteProvider::class)->execute();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
86
app/Http/Controllers/PersonController.php
Executable file
86
app/Http/Controllers/PersonController.php
Executable file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\People\DeletePeople;
|
||||
use App\Actions\People\GetPersonCredits;
|
||||
use App\Actions\People\PaginatePeople;
|
||||
use App\Jobs\IncrementModelViews;
|
||||
use App\Models\Person;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class PersonController extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('index', Person::class);
|
||||
|
||||
$pagination = (new PaginatePeople())->execute(request()->all());
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function show($id, $name = null)
|
||||
{
|
||||
$this->authorize('show', Person::class);
|
||||
|
||||
$loader = request('loader', 'personPage');
|
||||
|
||||
if (is_numeric($id) || ctype_digit($id)) {
|
||||
$person = Person::findOrFail($id);
|
||||
} else {
|
||||
$person = Person::firstOrCreateFromEncodedTmdbId($id);
|
||||
}
|
||||
|
||||
if ($loader === 'personPage' && requestIsFromFrontend()) {
|
||||
$person->maybeUpdateFromExternal();
|
||||
}
|
||||
|
||||
$data = array_merge(
|
||||
['person' => $person, 'loader' => $loader],
|
||||
app(GetPersonCredits::class)->execute($person),
|
||||
);
|
||||
|
||||
(new IncrementModelViews())->execute($data['person']);
|
||||
|
||||
return $this->renderClientOrApi([
|
||||
'data' => $data,
|
||||
'pageName' => 'person-page',
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$this->authorize('store', Person::class);
|
||||
|
||||
$data = request()->all();
|
||||
$data['popularity'] = Arr::get($data, 'popularity') ?: 50;
|
||||
$person = Person::create($data);
|
||||
|
||||
return $this->success(['person' => $person]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$this->authorize('update', Person::class);
|
||||
|
||||
$person = Person::findOrFail($id);
|
||||
|
||||
$data = request()->all();
|
||||
$data['popularity'] = Arr::get($data, 'popularity') ?: 50;
|
||||
$person->fill($data)->save();
|
||||
|
||||
return $this->success(['person' => $person]);
|
||||
}
|
||||
|
||||
public function destroy(string $ids)
|
||||
{
|
||||
$ids = explode(',', $ids);
|
||||
$this->authorize('destroy', Person::class);
|
||||
|
||||
app(DeletePeople::class)->execute($ids);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/PersonCreditsController.php
Executable file
30
app/Http/Controllers/PersonCreditsController.php
Executable file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\People\GetPersonCredits;
|
||||
use App\Models\Person;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class PersonCreditsController extends BaseController
|
||||
{
|
||||
public function fullTitleCredits(
|
||||
Person $person,
|
||||
int $titleId,
|
||||
string $department,
|
||||
) {
|
||||
$this->authorize('show', Person::class);
|
||||
|
||||
$credits = app(GetPersonCredits::class)->execute($person, [
|
||||
'titleId' => $titleId,
|
||||
]);
|
||||
|
||||
$title = Arr::first(
|
||||
$credits['credits'][$department],
|
||||
fn($title) => $title['id'] === (int) $titleId,
|
||||
);
|
||||
|
||||
return $this->success(['credits' => $title['episodes']]);
|
||||
}
|
||||
}
|
||||
24
app/Http/Controllers/RelatedTitlesController.php
Executable file
24
app/Http/Controllers/RelatedTitlesController.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Titles\Retrieve\GetRelatedTitles;
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class RelatedTitlesController extends BaseController
|
||||
{
|
||||
public function index(int $id)
|
||||
{
|
||||
$this->authorize('index', Title::class);
|
||||
|
||||
$title = Title::with('keywords', 'genres')->findOrFail($id);
|
||||
|
||||
$related = app(GetRelatedTitles::class)->execute(
|
||||
$title,
|
||||
request()->all(),
|
||||
);
|
||||
|
||||
return $this->success(['titles' => $related]);
|
||||
}
|
||||
}
|
||||
135
app/Http/Controllers/ReviewController.php
Executable file
135
app/Http/Controllers/ReviewController.php
Executable file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Reviews\UpdateReviewableAverageScore;
|
||||
use App\Models\Review;
|
||||
use Auth;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ReviewController extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('index', Review::class);
|
||||
|
||||
$builder = Review::withCount('reports')->withTextOnly();
|
||||
|
||||
// will need to specify this outside of filters on edit title reviews page
|
||||
if (request('reviewable_id') && request('reviewable_type')) {
|
||||
$builder->where([
|
||||
'reviewable_id' => request('reviewable_id'),
|
||||
'reviewable_type' => request('reviewable_type'),
|
||||
]);
|
||||
}
|
||||
|
||||
$datasource = new Datasource($builder, request()->all());
|
||||
$order = $datasource->getOrder();
|
||||
|
||||
if (Str::endsWith($order['col'], 'mostHelpful')) {
|
||||
$datasource->order = false;
|
||||
$builder->orderByMostHelpful();
|
||||
}
|
||||
|
||||
$pagination = $datasource->paginate()->through(function ($review) {
|
||||
if ($review->relationLoaded('reviewable') && $review->reviewable) {
|
||||
$normalized = $review->reviewable->toNormalizedArray();
|
||||
$review->unsetRelation('reviewable');
|
||||
$review->setAttribute('reviewable', $normalized);
|
||||
}
|
||||
return $review;
|
||||
});
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$review = Review::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $review);
|
||||
|
||||
$data = request()->all();
|
||||
|
||||
if (isset($data['body'])) {
|
||||
$data['has_text'] = true;
|
||||
}
|
||||
|
||||
$review->fill($data)->save();
|
||||
|
||||
app(UpdateReviewableAverageScore::class)->execute(
|
||||
$review->reviewable_id,
|
||||
$review->reviewable_type,
|
||||
);
|
||||
|
||||
return $this->success(['review' => $review]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$this->authorize('store', Review::class);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'reviewable_id' => 'required|integer',
|
||||
'reviewable_type' => 'required|string',
|
||||
'title' => 'string|min:10|max:150',
|
||||
'body' => 'string|min:100|max:5000',
|
||||
'score' => 'required|integer|min:1|max:10',
|
||||
]);
|
||||
|
||||
$reviewableId = $data['reviewable_id'];
|
||||
$reviewableType = $data['reviewable_type'];
|
||||
|
||||
$values = [
|
||||
'score' => request('score'),
|
||||
];
|
||||
|
||||
// don't override review body or title when only score changes
|
||||
if (request('body')) {
|
||||
$values['body'] = request('body');
|
||||
$values['has_text'] = true;
|
||||
}
|
||||
if (request('title')) {
|
||||
$values['title'] = request('title');
|
||||
}
|
||||
|
||||
$review = Review::updateOrCreate(
|
||||
[
|
||||
'user_id' => Auth::id(),
|
||||
'reviewable_type' => $reviewableType,
|
||||
'reviewable_id' => $reviewableId,
|
||||
],
|
||||
$values,
|
||||
);
|
||||
|
||||
$review->load('user');
|
||||
app(UpdateReviewableAverageScore::class)->execute(
|
||||
$reviewableId,
|
||||
$reviewableType,
|
||||
);
|
||||
|
||||
return $this->success(['review' => $review]);
|
||||
}
|
||||
|
||||
public function destroy(string $ids)
|
||||
{
|
||||
$reviewIds = explode(',', $ids);
|
||||
|
||||
$reviews = Review::whereIn('id', $reviewIds)->get();
|
||||
|
||||
$this->authorize('destroy', [Review::class, $reviews]);
|
||||
|
||||
$reviews->each(function (Review $review) {
|
||||
app(UpdateReviewableAverageScore::class)->execute(
|
||||
$review->reviewable_id,
|
||||
$review->reviewable_type,
|
||||
);
|
||||
});
|
||||
|
||||
Review::whereIn('id', $reviews->pluck('id'))->delete();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/ReviewFeedbackController.php
Executable file
44
app/Http/Controllers/ReviewFeedbackController.php
Executable file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Review;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class ReviewFeedbackController extends BaseController
|
||||
{
|
||||
public function store(Review $review)
|
||||
{
|
||||
$this->authorize('show', $review);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'is_helpful' => 'required|boolean',
|
||||
]);
|
||||
$isHelpful = $data['is_helpful'];
|
||||
|
||||
$review->feedback()->updateOrCreate(
|
||||
[
|
||||
'user_id' => auth()->id(),
|
||||
],
|
||||
[
|
||||
'is_helpful' => $isHelpful,
|
||||
],
|
||||
);
|
||||
|
||||
$review->timestamps = false;
|
||||
|
||||
if ($isHelpful) {
|
||||
$review->increment('helpful_count');
|
||||
if ($review->not_helpful_count > 0) {
|
||||
$review->decrement('not_helpful_count');
|
||||
}
|
||||
} else {
|
||||
if ($review->helpful_count > 0) {
|
||||
$review->decrement('helpful_count');
|
||||
}
|
||||
$review->increment('not_helpful_count');
|
||||
}
|
||||
|
||||
return $this->success(['review' => $review->load('feedback')]);
|
||||
}
|
||||
}
|
||||
29
app/Http/Controllers/ReviewableController.php
Executable file
29
app/Http/Controllers/ReviewableController.php
Executable file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Loaders\ReviewsLoader;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class ReviewableController extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$data = (new ReviewsLoader())->loadData([
|
||||
'reviewableType' => request('reviewable_type'),
|
||||
'reviewableId' => request('reviewable_id'),
|
||||
'page' => request('page'),
|
||||
'orderBy' => request('orderBy'),
|
||||
'orderDir' => request('orderDir'),
|
||||
'perPage' => request('perPage'),
|
||||
]);
|
||||
|
||||
if (!$data) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->authorize('show', $data['reviewable']);
|
||||
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
73
app/Http/Controllers/SearchController.php
Executable file
73
app/Http/Controllers/SearchController.php
Executable file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\LocalSearch;
|
||||
use App\Services\Data\Tmdb\TmdbApi;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Collection;
|
||||
use Str;
|
||||
|
||||
class SearchController extends BaseController
|
||||
{
|
||||
public function index($query)
|
||||
{
|
||||
$dataProvider =
|
||||
request('provider') ?: settings('content.search_provider');
|
||||
$results = $this->searchUsing($dataProvider, $query)
|
||||
->map(function ($result) {
|
||||
if (isset($result['description'])) {
|
||||
$result['description'] = Str::limit(
|
||||
$result['description'],
|
||||
140,
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
})
|
||||
->values();
|
||||
|
||||
$data = [
|
||||
'results' => $results,
|
||||
'query' => trim(strip_tags($query), '"\''),
|
||||
'loader' => 'searchPage',
|
||||
];
|
||||
|
||||
return $this->renderClientOrApi([
|
||||
'pageName' => 'search-page',
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
private function searchUsing($provider, $query)
|
||||
{
|
||||
$params = request()->all();
|
||||
$params['limit'] =
|
||||
request('loader', 'searchPage') === 'searchPage' ? 20 : 8;
|
||||
|
||||
if ($provider === 'tmdb') {
|
||||
return app(TmdbApi::class)->search($query, $params);
|
||||
}
|
||||
|
||||
$results = app(LocalSearch::class)->execute($query, $params);
|
||||
|
||||
if ($provider === 'all') {
|
||||
$tmdb = app(TmdbApi::class)->search($query, $params);
|
||||
$results = $results
|
||||
->concat($tmdb)
|
||||
->unique(
|
||||
fn($item) => ($item['tmdb_id'] ?: $item['name']) .
|
||||
$item['model_type'],
|
||||
)
|
||||
->groupBy('model_type')
|
||||
// make sure specified limit is enforced per group
|
||||
// (title, person) instead of the whole collection
|
||||
->map(
|
||||
fn(Collection $group) => $group->slice(0, $params['limit']),
|
||||
)
|
||||
->flatten(1)
|
||||
->sortByDesc('popularity');
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
59
app/Http/Controllers/SeasonController.php
Executable file
59
app/Http/Controllers/SeasonController.php
Executable file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Titles\DeleteSeasons;
|
||||
use App\Actions\Titles\LoadSeasonEpisodeNumbers;
|
||||
use App\Loaders\SeasonLoader;
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class SeasonController extends BaseController
|
||||
{
|
||||
public function show()
|
||||
{
|
||||
$data = (new SeasonLoader())->loadData(request('loader'));
|
||||
|
||||
$this->authorize('show', $data['title']);
|
||||
|
||||
return $this->renderClientOrApi([
|
||||
'pageName' => 'season-page',
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store($titleId)
|
||||
{
|
||||
$this->authorize('update', Title::class);
|
||||
|
||||
$title = Title::withCount('seasons')->findOrFail($titleId);
|
||||
|
||||
$season = $title->seasons()->create([
|
||||
'number' => $title->seasons_count + 1,
|
||||
]);
|
||||
|
||||
return $this->success(['season' => $season]);
|
||||
}
|
||||
|
||||
public function destroy(int $seasonId)
|
||||
{
|
||||
$this->authorize('update', Title::class);
|
||||
|
||||
app(DeleteSeasons::class)->execute([$seasonId]);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function episodeNumbers()
|
||||
{
|
||||
$titleId = request()->route('titleId');
|
||||
$seasonNumber = request()->route('seasonNumber');
|
||||
|
||||
$episodeNumbers = (new LoadSeasonEpisodeNumbers())->execute(
|
||||
$titleId,
|
||||
$seasonNumber,
|
||||
);
|
||||
|
||||
return $this->success(['episodeNumbers' => $episodeNumbers]);
|
||||
}
|
||||
}
|
||||
23
app/Http/Controllers/SeasonEpisodesController.php
Executable file
23
app/Http/Controllers/SeasonEpisodesController.php
Executable file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Titles\Retrieve\PaginateSeasonEpisodes;
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class SeasonEpisodesController extends BaseController
|
||||
{
|
||||
public function __invoke(Title $title, int $seasonNumber)
|
||||
{
|
||||
$this->authorize('show', $title);
|
||||
|
||||
$pagination = app(PaginateSeasonEpisodes::class)->execute(
|
||||
$title,
|
||||
$seasonNumber,
|
||||
request()->all(),
|
||||
);
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
}
|
||||
61
app/Http/Controllers/TitleAutocompleteController.php
Executable file
61
app/Http/Controllers/TitleAutocompleteController.php
Executable file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Titles\LoadSeasonEpisodeNumbers;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class TitleAutocompleteController extends BaseController
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
$this->authorize('index', Title::class);
|
||||
|
||||
$search = request('searchQuery');
|
||||
$selectedTitleId = request('selectedTitleId');
|
||||
$seasonNumber = request('seasonNumber');
|
||||
$builder = app(Title::class);
|
||||
|
||||
if ($search) {
|
||||
$builder = $builder->search($search);
|
||||
}
|
||||
|
||||
$results = $builder
|
||||
->take(10)
|
||||
->get(['id', 'name', 'poster', 'release_date']);
|
||||
|
||||
$results = $results->map(function (Title $title) {
|
||||
$normalized = $title->toNormalizedArray();
|
||||
if ($title->relationLoaded('season')) {
|
||||
$normalized['episodes_count'] =
|
||||
$title->season->episodes_count ?? 0;
|
||||
}
|
||||
$normalized['seasons_count'] = $title->seasons_count;
|
||||
return $normalized;
|
||||
});
|
||||
|
||||
if ($selectedTitleId) {
|
||||
$title = Title::find($selectedTitleId);
|
||||
if ($title) {
|
||||
$normalizedTitle = $title->toNormalizedArray();
|
||||
$normalizedTitle['seasons_count'] = Season::where(
|
||||
'title_id',
|
||||
$title->id,
|
||||
)->count();
|
||||
if ($seasonNumber) {
|
||||
$normalizedTitle[
|
||||
'episode_numbers'
|
||||
] = (new LoadSeasonEpisodeNumbers())->execute(
|
||||
$title->id,
|
||||
$seasonNumber,
|
||||
);
|
||||
}
|
||||
$results->prepend($normalizedTitle);
|
||||
}
|
||||
}
|
||||
|
||||
return ['titles' => $results];
|
||||
}
|
||||
}
|
||||
70
app/Http/Controllers/TitleController.php
Executable file
70
app/Http/Controllers/TitleController.php
Executable file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Titles\DeleteTitles;
|
||||
use App\Actions\Titles\Retrieve\PaginateTitles;
|
||||
use App\Actions\Titles\Store\StoreTitleData;
|
||||
use App\Jobs\IncrementModelViews;
|
||||
use App\Loaders\TitleLoader;
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class TitleController extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('index', Title::class);
|
||||
|
||||
$pagination = app(PaginateTitles::class)->execute(request()->all());
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
$data = (new TitleLoader())->loadData(request('loader'));
|
||||
|
||||
$this->authorize('show', $data['title']);
|
||||
|
||||
(new IncrementModelViews())->execute($data['title']);
|
||||
|
||||
return $this->renderClientOrApi([
|
||||
'pageName' => 'title-page',
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(int $id)
|
||||
{
|
||||
$this->authorize('update', Title::class);
|
||||
|
||||
$data = request()->all();
|
||||
$title = Title::findOrFail($id);
|
||||
|
||||
$title = app(StoreTitleData::class)->execute($title, $data, [
|
||||
'overrideWithEmptyValues' => true,
|
||||
]);
|
||||
|
||||
return $this->success(['title' => $title]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$this->authorize('store', Title::class);
|
||||
|
||||
$title = Title::create(request()->all());
|
||||
|
||||
return $this->success(['title' => $title]);
|
||||
}
|
||||
|
||||
public function destroy(string $ids)
|
||||
{
|
||||
$titleIds = explode(',', $ids);
|
||||
$this->authorize('destroy', Title::class);
|
||||
|
||||
app(DeleteTitles::class)->execute($titleIds);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
115
app/Http/Controllers/TitleCreditsController.php
Executable file
115
app/Http/Controllers/TitleCreditsController.php
Executable file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Season;
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TitleCreditsController extends BaseController
|
||||
{
|
||||
public function index(Title $title)
|
||||
{
|
||||
$this->authorize('show', $title);
|
||||
|
||||
$model = $this->resolveCreditableModel($title, request()->all());
|
||||
|
||||
$builder = $model->credits();
|
||||
|
||||
if (request('crewOnly')) {
|
||||
$builder->wherePivot('department', '!=', 'actors');
|
||||
}
|
||||
|
||||
if ($department = request('department')) {
|
||||
$builder->wherePivot('department', $department);
|
||||
}
|
||||
|
||||
$datasource = new Datasource($builder, request()->all());
|
||||
$datasource->order = false;
|
||||
|
||||
return $this->success(['pagination' => $datasource->paginate()]);
|
||||
}
|
||||
|
||||
public function update(Title $title, int $pivotId)
|
||||
{
|
||||
$this->authorize('update', $title);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'character' => 'string|nullable',
|
||||
'department' => 'string|nullable',
|
||||
'job' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$model = $this->resolveCreditableModel($title, request()->all());
|
||||
$model->updateCredit($pivotId, $data);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function store(Title $title)
|
||||
{
|
||||
$this->authorize('update', $title);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'person_id' => 'required|integer|exists:people,id',
|
||||
'character' => 'required_if:department,cast|string',
|
||||
'department' => 'required|string',
|
||||
'job' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$model = $this->resolveCreditableModel($title, request()->all());
|
||||
$model->createCredit($data);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function destroy(Title $title, int $pivotId)
|
||||
{
|
||||
$this->authorize('update', $title);
|
||||
|
||||
$model = $this->resolveCreditableModel($title, request()->all());
|
||||
$model
|
||||
->credits()
|
||||
->wherePivot('id', $pivotId)
|
||||
->detach();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function changeOrder()
|
||||
{
|
||||
$this->authorize('update', Title::class);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'ids' => 'array|min:1',
|
||||
'ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
$queryPart = '';
|
||||
foreach ($data['ids'] as $order => $id) {
|
||||
$queryPart .= " when id=$id then $order";
|
||||
}
|
||||
|
||||
DB::table('creditables')
|
||||
->whereIn('id', $data['ids'])
|
||||
->update(['order' => DB::raw("(case $queryPart end)")]);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function resolveCreditableModel(
|
||||
Title $title,
|
||||
array $params,
|
||||
): Title|Season|Episode {
|
||||
if (Arr::get($params, 'season') && Arr::get($params, 'episode')) {
|
||||
return $title->findEpisode($params['season'], $params['episode']);
|
||||
} elseif (Arr::get($params, 'season')) {
|
||||
return $title->findSeason($params['season']);
|
||||
}
|
||||
return $title;
|
||||
}
|
||||
}
|
||||
20
app/Http/Controllers/TitleNewsController.php
Executable file
20
app/Http/Controllers/TitleNewsController.php
Executable file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class TitleNewsController extends BaseController
|
||||
{
|
||||
public function __invoke(Title $title)
|
||||
{
|
||||
$this->authorize('show', $title);
|
||||
|
||||
$articles = $title->load([
|
||||
'newsArticles' => fn($q) => $q->limit(4),
|
||||
])->newsArticles;
|
||||
|
||||
return $this->success(['news_articles' => $articles]);
|
||||
}
|
||||
}
|
||||
22
app/Http/Controllers/TitleSeasonsController.php
Executable file
22
app/Http/Controllers/TitleSeasonsController.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Titles\Retrieve\PaginateTitleSeasons;
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class TitleSeasonsController extends BaseController
|
||||
{
|
||||
public function __invoke(Title $title)
|
||||
{
|
||||
$this->authorize('show', $title);
|
||||
|
||||
$pagination = app(PaginateTitleSeasons::class)->execute(
|
||||
$title,
|
||||
request()->all(),
|
||||
);
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
}
|
||||
46
app/Http/Controllers/TitleTagsController.php
Executable file
46
app/Http/Controllers/TitleTagsController.php
Executable file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TitleTagsController extends BaseController
|
||||
{
|
||||
public function store(Title $title, string $type)
|
||||
{
|
||||
$this->authorize('update', $title);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'tag_name' => 'required|string',
|
||||
]);
|
||||
|
||||
$relation = $this->getRelationName($type);
|
||||
|
||||
$tags = $title
|
||||
->$relation()
|
||||
->getModel()
|
||||
->insertOrRetrieve([$data['tag_name']]);
|
||||
|
||||
$title->$relation()->syncWithoutDetaching($tags->pluck('id'));
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function destroy(Title $title, string $type, int $tagId)
|
||||
{
|
||||
$this->authorize('update', $title);
|
||||
|
||||
$relation = $this->getRelationName($type);
|
||||
|
||||
$title->$relation()->detach([$tagId]);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
private function getRelationName($type)
|
||||
{
|
||||
return Str::plural(Str::camel($type));
|
||||
}
|
||||
}
|
||||
147
app/Http/Controllers/UserProfileController.php
Executable file
147
app/Http/Controllers/UserProfileController.php
Executable file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Lists\ListsLoader;
|
||||
use App\Models\Episode;
|
||||
use App\Models\Title;
|
||||
use App\Models\User;
|
||||
use Auth;
|
||||
use Common\Auth\Events\UserAvatarChanged;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class UserProfileController extends BaseController
|
||||
{
|
||||
public function show(User $user)
|
||||
{
|
||||
$this->authorize('show', $user);
|
||||
|
||||
$user->load(['profile', 'links']);
|
||||
$user->loadCount(['followers', 'followedUsers', 'lists']);
|
||||
|
||||
$user->is_pro = $user->subscribed();
|
||||
|
||||
return $this->success(['user' => $user]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$user = Auth::user();
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'user' => 'array',
|
||||
'profile' => 'array',
|
||||
'links' => 'array',
|
||||
]);
|
||||
|
||||
User::unguard(true);
|
||||
$oldAvatar = $user->avatar;
|
||||
$user->fill($data['user'])->save();
|
||||
|
||||
if (
|
||||
isset($data['user']['avatar']) &&
|
||||
$oldAvatar !== $data['user']['avatar']
|
||||
) {
|
||||
event(new UserAvatarChanged($user));
|
||||
}
|
||||
|
||||
$profile = $user
|
||||
->profile()
|
||||
->updateOrCreate(['user_id' => $user->id], $data['profile']);
|
||||
|
||||
$user->links()->delete();
|
||||
$links = $user->links()->createMany($data['links']);
|
||||
|
||||
$user->setRelation('profile', $profile);
|
||||
$user->setRelation('links', $links);
|
||||
|
||||
return $this->success(['user' => $user]);
|
||||
}
|
||||
|
||||
public function lists(User $user)
|
||||
{
|
||||
$this->authorize('show', $user);
|
||||
|
||||
$pagination = (new ListsLoader())->forUser($user, request()->all());
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function ratings(User $user)
|
||||
{
|
||||
$this->authorize('show', $user);
|
||||
|
||||
$datasource = new Datasource(
|
||||
$user
|
||||
->reviews()
|
||||
->whereNull('body')
|
||||
->with([
|
||||
'reviewable' => function (MorphTo $morphTo) {
|
||||
$morphTo
|
||||
->morphWith([
|
||||
Episode::class => ['title'],
|
||||
])
|
||||
->with('primaryVideo');
|
||||
},
|
||||
'user',
|
||||
]),
|
||||
request()->all(),
|
||||
);
|
||||
|
||||
$pagination = $datasource->paginate();
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function reviews(User $user)
|
||||
{
|
||||
$this->authorize('show', $user);
|
||||
|
||||
$datasource = new Datasource(
|
||||
$user
|
||||
->reviews()
|
||||
->where('reviewable_type', Title::MODEL_TYPE)
|
||||
->whereNotNull('body')
|
||||
->with([
|
||||
'reviewable' => function (MorphTo $morphTo) {
|
||||
$morphTo->morphWith([
|
||||
Episode::class => ['title'],
|
||||
]);
|
||||
},
|
||||
'user',
|
||||
]),
|
||||
request()->all(),
|
||||
);
|
||||
|
||||
$pagination = $datasource->paginate();
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function comments(User $user)
|
||||
{
|
||||
$this->authorize('show', $user);
|
||||
|
||||
$datasource = new Datasource(
|
||||
$user
|
||||
->comments()
|
||||
->with([
|
||||
'commentable' => function (MorphTo $morphTo) {
|
||||
$morphTo->morphWith([
|
||||
Episode::class => ['title'],
|
||||
]);
|
||||
},
|
||||
'user',
|
||||
])
|
||||
->where('deleted', false),
|
||||
request()->all(),
|
||||
);
|
||||
|
||||
$pagination = $datasource->paginate();
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
}
|
||||
46
app/Http/Controllers/UserRatingsController.php
Executable file
46
app/Http/Controllers/UserRatingsController.php
Executable file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class UserRatingsController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
$ratings = Auth::user()
|
||||
->reviews()
|
||||
->select(['id', 'reviewable_id', 'reviewable_type', 'score'])
|
||||
->limit(1000)
|
||||
->get()
|
||||
->map(
|
||||
fn($review) => [
|
||||
'id' => $review->id,
|
||||
'score' => $review->score,
|
||||
'reviewable_id' => $review->reviewable_id,
|
||||
'type' => $review->reviewable_type,
|
||||
],
|
||||
)
|
||||
->groupBy('type')
|
||||
->map(
|
||||
fn($group) => $group
|
||||
->mapWithKeys(
|
||||
fn($item) => [
|
||||
$item['reviewable_id'] => [
|
||||
'id' => $item['id'],
|
||||
'score' => $item['score'],
|
||||
],
|
||||
],
|
||||
)
|
||||
->all(),
|
||||
);
|
||||
|
||||
return $this->success(['ratings' => $ratings]);
|
||||
}
|
||||
}
|
||||
47
app/Http/Controllers/UserWatchlistController.php
Executable file
47
app/Http/Controllers/UserWatchlistController.php
Executable file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UserWatchlistController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
$list = Auth::user()
|
||||
->watchlist()
|
||||
->firstOrFail();
|
||||
|
||||
$items = DB::table('channelables')
|
||||
->where('channel_id', $list->id)
|
||||
->pluck('channelable_type', 'channelable_id')
|
||||
->map(
|
||||
fn($modelType, $itemId) => [
|
||||
'id' => $itemId,
|
||||
'type' => $modelType,
|
||||
],
|
||||
)
|
||||
->groupBy('type')
|
||||
->map(
|
||||
fn($group) => $group->mapWithKeys(
|
||||
fn($item) => [
|
||||
$item['id'] => true,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return $this->success([
|
||||
'watchlist' => [
|
||||
'id' => $list->id,
|
||||
'items' => $items,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/VideoApproveController.php
Executable file
27
app/Http/Controllers/VideoApproveController.php
Executable file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Video;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class VideoApproveController extends BaseController
|
||||
{
|
||||
public function approve(Video $video)
|
||||
{
|
||||
$this->authorize('update', $video);
|
||||
|
||||
$video->update(['approved' => true]);
|
||||
|
||||
return $this->success(['video' => $video]);
|
||||
}
|
||||
|
||||
public function disapprove(Video $video)
|
||||
{
|
||||
$this->authorize('update', $video);
|
||||
|
||||
$video->update(['approved' => false]);
|
||||
|
||||
return $this->success(['video' => $video]);
|
||||
}
|
||||
}
|
||||
33
app/Http/Controllers/VideoOrderController.php
Executable file
33
app/Http/Controllers/VideoOrderController.php
Executable file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Title;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class VideoOrderController extends BaseController
|
||||
{
|
||||
public function changeOrder(int $titleId)
|
||||
{
|
||||
$title = Title::findOrFail($titleId);
|
||||
|
||||
$this->authorize('update', $title);
|
||||
|
||||
request()->validate([
|
||||
'ids' => 'array|min:1',
|
||||
'ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
$queryPart = '';
|
||||
foreach (request('ids') as $order => $id) {
|
||||
$queryPart .= " when id=$id then $order";
|
||||
}
|
||||
|
||||
DB::table('videos')
|
||||
->whereIn('id', request('ids'))
|
||||
->update(['order' => DB::raw("(case $queryPart end)")]);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
35
app/Http/Controllers/VideoReportController.php
Executable file
35
app/Http/Controllers/VideoReportController.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Video;
|
||||
use Auth;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class VideoReportController extends BaseController
|
||||
{
|
||||
public function report(Video $video)
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$userIp = $this->request->ip();
|
||||
|
||||
// if we can't match current user, bail
|
||||
if ( ! $userId && ! $userIp) return null;
|
||||
|
||||
$alreadyReported = $video->reports()
|
||||
->where(function(Builder $query) use($userId, $userIp) {
|
||||
$query->where('user_id', $userId)->orWhere('user_ip', $userIp);
|
||||
})->first();
|
||||
|
||||
if ($alreadyReported) {
|
||||
return $this->error(__('You have already reported this video.'));
|
||||
} else {
|
||||
$report = $video->reports()->create([
|
||||
'user_id' => $userId,
|
||||
'user_ip' => $userIp
|
||||
]);
|
||||
return $this->success(['report' => $report]);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
app/Http/Controllers/VideosController.php
Executable file
172
app/Http/Controllers/VideosController.php
Executable file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Plays\LogVideoPlay;
|
||||
use App\Actions\Videos\CrupdateVideo;
|
||||
use App\Models\Video;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Common\Database\Datasource\DatasourceFilters;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class VideosController extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('index', Video::class);
|
||||
|
||||
$builder = Video::with([
|
||||
'captions',
|
||||
'title' => fn(BelongsTo $query) => $query->with('seasons'),
|
||||
])->withCount(['reports', 'plays']);
|
||||
|
||||
$filters = new DatasourceFilters(request('filters'));
|
||||
|
||||
if (
|
||||
$titleFilter =
|
||||
$filters->getAndRemove('title_id') ?? request('title_id')
|
||||
) {
|
||||
$titleId = $titleFilter['value'] ?? request('title_id');
|
||||
$season = $titleFilter['season'] ?? request('season');
|
||||
$episode = $titleFilter['episode'] ?? request('episode');
|
||||
$builder->where('title_id', $titleId);
|
||||
$builder->when($season, fn($q) => $q->where('season_num', $season));
|
||||
$builder->when(
|
||||
$episode,
|
||||
fn($q) => $q->where('episode_num', $episode),
|
||||
);
|
||||
}
|
||||
|
||||
$datasource = new Datasource($builder, request()->all(), $filters);
|
||||
$order = $datasource->getOrder();
|
||||
|
||||
if (Str::endsWith($order['col'], 'upvotes')) {
|
||||
$datasource->order = false;
|
||||
$builder->orderByMostUpvotes();
|
||||
}
|
||||
|
||||
// order by percentage of likes, taking into account total amount of likes and dislikes
|
||||
if (Str::endsWith($order['col'], 'score')) {
|
||||
$datasource->order = false;
|
||||
$builder->orderByWeightedScore();
|
||||
}
|
||||
|
||||
// add a secondary order by episode number if ordering by season number
|
||||
if (Str::endsWith($order['col'], 'season_num')) {
|
||||
$datasource->order = false;
|
||||
$builder
|
||||
->orderBy('season_num', 'desc')
|
||||
->orderBy('episode_num', 'desc');
|
||||
}
|
||||
|
||||
return $this->success(['pagination' => $datasource->paginate()]);
|
||||
}
|
||||
|
||||
public function show(Video $video)
|
||||
{
|
||||
$this->authorize('show', Video::class);
|
||||
|
||||
$video->load(['captions', 'title']);
|
||||
|
||||
return $this->success(['video' => $video]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$this->authorize('store', Video::class);
|
||||
|
||||
$this->validate(
|
||||
request(),
|
||||
[
|
||||
'title_id' => 'required|integer',
|
||||
'name' => ['required', 'string', 'min:3', 'max:250'],
|
||||
'src' => 'required|max:1000',
|
||||
'type' => 'required|string|min:3|max:250',
|
||||
'category' => 'required|string|min:3|max:20',
|
||||
'quality' => 'nullable|string|min:2|max:250',
|
||||
'language' => 'required|nullable|string|max:10',
|
||||
'season_num' => 'nullable|integer',
|
||||
'episode_num' => 'requiredWith:season|integer|nullable',
|
||||
'captions' => 'nullable|array',
|
||||
'captions.*.name' => 'required|string|max:100',
|
||||
'captions.*.url' => 'required|string|max:250',
|
||||
'captions.*.language' => 'required|string|max:100',
|
||||
],
|
||||
[
|
||||
'title_id.*' => __(
|
||||
'Select a title this video should be attached to.',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
$video = app(CrupdateVideo::class)->execute(request()->all());
|
||||
|
||||
return $this->success(['video' => $video]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$this->authorize('update', Video::class);
|
||||
|
||||
$this->validate(
|
||||
request(),
|
||||
[
|
||||
'name' => 'string|min:3|max:250',
|
||||
'src' => 'required|max:1000',
|
||||
'type' => 'string|min:3|max:1000',
|
||||
'quality' => 'nullable|string|min:2|max:250',
|
||||
'language' => 'required|nullable|string|max:10',
|
||||
'title_id' => 'integer',
|
||||
'season_num' => 'nullable|integer',
|
||||
'episode_num' => 'requiredWith:season|integer|nullable',
|
||||
'captions' => 'nullable|array',
|
||||
'captions.*.name' => 'required|string|max:100',
|
||||
'captions.*.url' => 'required|string|max:250',
|
||||
'captions.*.language' => 'required|string|max:100',
|
||||
],
|
||||
[
|
||||
'title_id.*' => __(
|
||||
'Select a title this video should be attached to.',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
$video = app(CrupdateVideo::class)->execute(request()->all(), $id);
|
||||
|
||||
return $this->success(['video' => $video]);
|
||||
}
|
||||
|
||||
public function destroy($ids)
|
||||
{
|
||||
$ids = explode(',', $ids);
|
||||
$this->authorize('destroy', [Video::class, $ids]);
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$video = Video::find($id);
|
||||
if (is_null($video)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$video->delete();
|
||||
}
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function logPlay(Video $video)
|
||||
{
|
||||
$this->authorize('show', Video::class);
|
||||
|
||||
if (request()->getContentType() === 'application/json') {
|
||||
$data = request()->all();
|
||||
} else {
|
||||
$data = json_decode(request()->getContent(), true);
|
||||
}
|
||||
|
||||
app(LogVideoPlay::class)->execute($video, $data);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user