mirror of
https://github.com/AnimeThemes/animethemes-server.git
synced 2026-07-11 01:24:46 +02:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99ec12c690 | |||
| 979773d476 | |||
| ff2de264c8 | |||
| 3151921c58 | |||
| 118dd45085 | |||
| 5517d4061e | |||
| 952d3775f9 | |||
| 43c97f1a7c | |||
| d8354c536a | |||
| 80899b07df | |||
| d71c675b23 | |||
| bb667d0b45 | |||
| e812b94b87 | |||
| 9a0989a3f7 | |||
| 00e03a9f38 | |||
| 146833338d | |||
| 298c4b31d8 | |||
| e973f68537 | |||
| 3a77f95e49 | |||
| c18006badb | |||
| 2d17f7b9f7 | |||
| 5f915c86ea | |||
| d1a4e99303 | |||
| 191a5e890c | |||
| 07eb52b68f | |||
| 39f665a524 | |||
| 4258e0727d | |||
| 06ee91f968 | |||
| 95f8229b50 | |||
| 3c128ff036 | |||
| 9975feb7ec | |||
| ad6eca731b | |||
| 211c164908 | |||
| ee1f93f011 | |||
| 8818bd0fd2 | |||
| 1611c73001 | |||
| b05e61e4ac | |||
| 57d5d9b2ef | |||
| 9c569bc435 | |||
| ea8a644aa3 | |||
| ad07aa3703 | |||
| 101feb031f | |||
| f5942d0de5 | |||
| 4b01e3ac17 | |||
| ccdf3b423a | |||
| 6a6d28c9c7 | |||
| ebfa4f96af | |||
| 899d01fada | |||
| 3ed878ab46 |
+88
-32
@@ -10,6 +10,15 @@ APP_URL=http://localhost
|
||||
ASSET_URL=null
|
||||
APP_KEY=
|
||||
|
||||
# audio
|
||||
AUDIO_DISK_DEFAULT=audios_nyc
|
||||
AUDIO_DISKS=audios_nyc
|
||||
AUDIO_DISK_ROOT=
|
||||
AUDIO_PATH=/audio
|
||||
AUDIO_URL=
|
||||
AUDIO_STREAMING_METHOD=
|
||||
AUDIO_NGINX_REDIRECT=
|
||||
|
||||
# audit
|
||||
AUDITING_ENABLED=true
|
||||
|
||||
@@ -18,10 +27,17 @@ BROADCAST_DRIVER=log
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_HOST=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
ABLY_KEY=
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
# cache
|
||||
CACHE_DRIVER=file
|
||||
@@ -58,11 +74,8 @@ REDIS_DB=0
|
||||
REDIS_CACHE_DB=1
|
||||
|
||||
# elastic client
|
||||
ELASTIC_HOST=localhost
|
||||
ELASTIC_PORT=9200
|
||||
ELASTIC_SCHEME=http
|
||||
ELASTIC_USER=
|
||||
ELASTIC_PASS=
|
||||
ELASTIC_CONNECTION=default
|
||||
ELASTIC_HOST=http://localhost:9200
|
||||
|
||||
# elastic driver
|
||||
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
@@ -70,16 +83,9 @@ ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
# elastic migrations
|
||||
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
|
||||
|
||||
# enlightn
|
||||
ENLIGHTN_SKIP_ENVIRONMENT_SPECIFIC=false
|
||||
ENLIGHTN_USERNAME=
|
||||
ENLIGHTN_API_TOKEN=
|
||||
ENLIGHTN_GITHUB_REPO=
|
||||
|
||||
# feature flags
|
||||
ALLOW_VIDEO_STREAMS=false
|
||||
ALLOW_DISCORD_NOTIFICATIONS=false
|
||||
ALLOW_VIEW_RECORDING=false
|
||||
# ffmpeg
|
||||
FFMPEG_BINARIES=
|
||||
FFPROBE_BINARIES=
|
||||
|
||||
# filesystems
|
||||
FILESYSTEM_DISK=local
|
||||
@@ -94,14 +100,51 @@ IMAGE_DISABLE_ASSERTS=
|
||||
IMAGE_VISIBILITY=
|
||||
IMAGE_URL=
|
||||
|
||||
VIDEO_ACCESS_KEY_ID=
|
||||
VIDEO_SECRET_ACCESS_KEY=
|
||||
VIDEO_DEFAULT_REGION=
|
||||
VIDEO_ENDPOINT=
|
||||
VIDEO_BUCKET=
|
||||
VIDEO_STREAM_READS=
|
||||
VIDEO_DISABLE_ASSERTS=
|
||||
VIDEO_VISIBILITY=
|
||||
VIDEO_NYC_ACCESS_KEY_ID=
|
||||
VIDEO_NYC_SECRET_ACCESS_KEY=
|
||||
VIDEO_NYC_DEFAULT_REGION=
|
||||
VIDEO_NYC_ENDPOINT=
|
||||
VIDEO_NYC_BUCKET=
|
||||
VIDEO_NYC_STREAM_READS=
|
||||
VIDEO_NYC_DISABLE_ASSERTS=
|
||||
VIDEO_NYC_VISIBILITY=
|
||||
|
||||
VIDEO_FRA_ACCESS_KEY_ID=
|
||||
VIDEO_FRA_SECRET_ACCESS_KEY=
|
||||
VIDEO_FRA_DEFAULT_REGION=
|
||||
VIDEO_FRA_ENDPOINT=
|
||||
VIDEO_FRA_BUCKET=
|
||||
VIDEO_FRA_STREAM_READS=
|
||||
VIDEO_FRA_DISABLE_ASSERTS=
|
||||
VIDEO_FRA_VISIBILITY=
|
||||
|
||||
AUDIO_NYC_ACCESS_KEY_ID=
|
||||
AUDIO_NYC_SECRET_ACCESS_KEY=
|
||||
AUDIO_NYC_DEFAULT_REGION=
|
||||
AUDIO_NYC_ENDPOINT=
|
||||
AUDIO_NYC_BUCKET=
|
||||
AUDIO_NYC_STREAM_READS=
|
||||
AUDIO_NYC_DISABLE_ASSERTS=
|
||||
AUDIO_NYC_VISIBILITY=
|
||||
|
||||
AUDIO_FRA_ACCESS_KEY_ID=
|
||||
AUDIO_FRA_SECRET_ACCESS_KEY=
|
||||
AUDIO_FRA_DEFAULT_REGION=
|
||||
AUDIO_FRA_ENDPOINT=
|
||||
AUDIO_FRA_BUCKET=
|
||||
AUDIO_FRA_STREAM_READS=
|
||||
AUDIO_FRA_DISABLE_ASSERTS=
|
||||
AUDIO_FRA_VISIBILITY=
|
||||
|
||||
# flags
|
||||
ALLOW_VIDEO_STREAMS=false
|
||||
ALLOW_AUDIO_STREAMS=false
|
||||
ALLOW_DISCORD_NOTIFICATIONS=false
|
||||
ALLOW_VIEW_RECORDING=false
|
||||
|
||||
# fortify
|
||||
FORTIFY_PATH=
|
||||
FORTIFY_URL=http://localhost
|
||||
|
||||
# hashing
|
||||
BCRYPT_ROUNDS=10
|
||||
@@ -110,8 +153,13 @@ BCRYPT_ROUNDS=10
|
||||
HORIZON_DOMAIN=
|
||||
HORIZON_PATH=horizon
|
||||
|
||||
# image
|
||||
IMAGE_DISK=images
|
||||
IMAGE_DISK_ROOT=
|
||||
|
||||
# jetstream
|
||||
TEAM_CREATOR_ID=
|
||||
JETSTREAM_PATH=
|
||||
JETSTREAM_URL=http://localhost
|
||||
|
||||
# logging
|
||||
LOG_CHANNEL=daily
|
||||
@@ -127,7 +175,7 @@ MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS=null
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
MAIL_SENDMAIL_PATH=/usr/sbin/sendmail
|
||||
MAIL_SENDMAIL_PATH="/usr/sbin/sendmail -bs -i"
|
||||
MAIL_LOG_CHANNEL=
|
||||
|
||||
# nova
|
||||
@@ -176,11 +224,6 @@ SESSION_STORE=null
|
||||
SESSION_DOMAIN=null
|
||||
SESSION_SECURE_COOKIE=
|
||||
|
||||
# teams
|
||||
HORIZON_TEAM_ID=
|
||||
NOVA_TEAM_ID=
|
||||
TELESCOPE_TEAM_ID=
|
||||
|
||||
# telescope
|
||||
TELESCOPE_DOMAIN=null
|
||||
TELESCOPE_PATH=telescope
|
||||
@@ -188,9 +231,22 @@ TELESCOPE_DRIVER=database
|
||||
TELESCOPE_ENABLED=true
|
||||
|
||||
# video
|
||||
VIDEO_DISK_DEFAULT=videos_nyc
|
||||
VIDEO_DISKS=videos_nyc
|
||||
VIDEO_DISK_ROOT=
|
||||
VIDEO_PATH=/video
|
||||
VIDEO_URL=
|
||||
VIDEO_STREAMING_METHOD=
|
||||
VIDEO_NGINX_REDIRECT=
|
||||
VIDEO_ENCODER_VERSION=
|
||||
VIDEO_UPLOAD_DISKS=
|
||||
|
||||
# web
|
||||
WEB_URL=http://localhost
|
||||
WEB_PATH=
|
||||
|
||||
# wiki
|
||||
WIKI_FEATURED_ENTRY=
|
||||
WIKI_FEATURED_VIDEO=
|
||||
WIKI_DONATE_URL=#
|
||||
WIKI_FAQ_URL=#
|
||||
|
||||
@@ -8,3 +8,4 @@
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
@@ -5,14 +5,14 @@ on: push
|
||||
jobs:
|
||||
static-analysis:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
tools: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, phpunit
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Copy .env
|
||||
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
|
||||
- name: Install Dependencies
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Install NPM dependencies
|
||||
run: npm install
|
||||
- name: Compile assets
|
||||
run: npm run production
|
||||
run: npm run build
|
||||
- name: Directory Permissions
|
||||
run: chmod -R 777 storage bootstrap/cache
|
||||
- name: Create Database
|
||||
|
||||
@@ -5,32 +5,29 @@ on: push
|
||||
jobs:
|
||||
test:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
tools: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, phpunit
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Copy .env
|
||||
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
||||
NOVA_PASSWORD: ${{ secrets.NOVA_PASSWORD }}
|
||||
ENLIGHTN_USERNAME: ${{ secrets.ENLIGHTN_USERNAME }}
|
||||
ENLIGHTN_PASSWORD: ${{ secrets.ENLIGHTN_PASSWORD }}
|
||||
run: |
|
||||
composer config "http-basic.nova.laravel.com" "$NOVA_USERNAME" "$NOVA_PASSWORD"
|
||||
composer config "http-basic.satis.laravel-enlightn.com" "$ENLIGHTN_USERNAME" "$ENLIGHTN_PASSWORD"
|
||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
- name: Generate key
|
||||
run: php artisan key:generate
|
||||
- name: Install NPM dependencies
|
||||
run: npm install
|
||||
- name: Compile assets
|
||||
run: npm run production
|
||||
run: npm run build
|
||||
- name: Directory Permissions
|
||||
run: chmod -R 777 storage bootstrap/cache
|
||||
- name: Create Database
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/node_modules
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/build
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/test.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Tests/badge.svg?branch=wiki" alt="tests"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/static-analysis.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Static%20Analysis/badge.svg?branch=wiki" alt="static-analysis"></a>
|
||||
<a href="https://github.styleci.io/repos/111264405?branch=wiki"><img src="https://github.styleci.io/repos/111264405/shield?branch=wiki" alt="StyleCI"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/test.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Tests/badge.svg?branch=main" alt="tests"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/static-analysis.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Static%20Analysis/badge.svg?branch=main" alt="static-analysis"></a>
|
||||
<a href="https://github.styleci.io/repos/111264405?branch=main"><img src="https://github.styleci.io/repos/111264405/shield?branch=main" alt="StyleCI"></a>
|
||||
<a href="https://discordapp.com/invite/m9zbVyQ"><img src="https://img.shields.io/discord/354388306580078594.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/blob/wiki/LICENSE"><img src="https://img.shields.io/github/license/AnimeThemes/animethemes-server"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/blob/main/LICENSE"><img src="https://img.shields.io/github/license/AnimeThemes/animethemes-server"></a>
|
||||
<a href="https://reddit.com/r/AnimeThemes"><img src="https://img.shields.io/reddit/subreddit-subscribers/AnimeThemes?style=social"></a>
|
||||
<a href="https://twitter.com/AnimeThemesMoe"><img src="https://img.shields.io/twitter/follow/AnimeThemesMoe?style=social"></a>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
|
||||
/**
|
||||
* Class ActionResult.
|
||||
*/
|
||||
class ActionResult
|
||||
{
|
||||
/**
|
||||
* Create a new action result instance.
|
||||
*
|
||||
* @param ActionStatus $status
|
||||
* @param string|null $message
|
||||
*/
|
||||
public function __construct(protected readonly ActionStatus $status, protected readonly ?string $message = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action result status.
|
||||
*
|
||||
* @return ActionStatus
|
||||
*/
|
||||
public function getStatus(): ActionStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action result message.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getMessage(): ?string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the action failed?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFailed(): bool
|
||||
{
|
||||
return ActionStatus::FAILED()->is($this->status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class BaseAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class BaseAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param TModel $model
|
||||
*/
|
||||
public function __construct(protected BaseModel $model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
abstract public function handle(): ActionResult;
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return TModel
|
||||
*/
|
||||
abstract protected function getModel(): BaseModel;
|
||||
|
||||
/**
|
||||
* Get the relation to resources.
|
||||
*
|
||||
* @return Relation
|
||||
*/
|
||||
abstract protected function relation(): Relation;
|
||||
|
||||
/**
|
||||
* Get the human-friendly label for the underlying model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function label(): string
|
||||
{
|
||||
return Str::headline(class_basename($this->getModel()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime;
|
||||
|
||||
use App\Actions\Models\Wiki\BackfillImageAction;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Image;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeImageAction.
|
||||
*
|
||||
* @extends BackfillImageAction<Anime>
|
||||
*/
|
||||
abstract class BackfillAnimeImageAction extends BackfillImageAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function __construct(Anime $anime)
|
||||
{
|
||||
parent::__construct($anime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Anime
|
||||
*/
|
||||
protected function getModel(): Anime
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to images.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->images();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Image to Anime.
|
||||
*
|
||||
* @param Image $image
|
||||
* @return void
|
||||
*/
|
||||
protected function attachImage(Image $image): void
|
||||
{
|
||||
Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach($image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime;
|
||||
|
||||
use App\Actions\Models\Wiki\BackfillResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pivots\AnimeResource;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeResourceAction.
|
||||
*
|
||||
* @extends BackfillResourceAction<Anime>
|
||||
*/
|
||||
abstract class BackfillAnimeResourceAction extends BackfillResourceAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function __construct(Anime $anime)
|
||||
{
|
||||
parent::__construct($anime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Anime
|
||||
*/
|
||||
protected function getModel(): Anime
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to images.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->resources();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or Create Resource from response.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string|null $slug
|
||||
* @return ExternalResource
|
||||
*/
|
||||
protected function getOrCreateResource(int $id, string $slug = null): ExternalResource
|
||||
{
|
||||
$resource = ExternalResource::query()
|
||||
->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)
|
||||
->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id)
|
||||
->where(ExternalResource::ATTRIBUTE_LINK, ResourceSite::formatAnimeResourceLink($this->getSite(), $id, $slug))
|
||||
->first();
|
||||
|
||||
if ($resource === null) {
|
||||
Log::info("Creating {$this->getSite()->description} Resource '$id'");
|
||||
|
||||
$resource = ExternalResource::query()->create([
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id,
|
||||
ExternalResource::ATTRIBUTE_LINK => ResourceSite::formatAnimeResourceLink($this->getSite(), $id, $slug),
|
||||
ExternalResource::ATTRIBUTE_SITE => $this->getSite()->value,
|
||||
]);
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Resource to Anime.
|
||||
*
|
||||
* @param ExternalResource $resource
|
||||
* @return void
|
||||
*/
|
||||
protected function attachResource(ExternalResource $resource): void
|
||||
{
|
||||
if (AnimeResource::query()
|
||||
->where($this->getModel()->getKeyName(), $this->getModel()->getKey())
|
||||
->where($resource->getKeyName(), $resource->getKey())
|
||||
->doesntExist()
|
||||
) {
|
||||
Log::info("Attaching Resource '{$resource->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach($resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-10
@@ -2,21 +2,21 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Image;
|
||||
namespace App\Actions\Models\Wiki\Anime\Image;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeImageAction;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeImage;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Class BackfillLargeCoverImage.
|
||||
* Class BackfillLargeCoverImageAction.
|
||||
*/
|
||||
class BackfillLargeCoverImage extends BackfillAnimeImage
|
||||
class BackfillLargeCoverImageAction extends BackfillAnimeImageAction
|
||||
{
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
@@ -37,12 +37,9 @@ class BackfillLargeCoverImage extends BackfillAnimeImage
|
||||
*/
|
||||
protected function getImage(): ?Image
|
||||
{
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbImage($anilistResource);
|
||||
if ($anidbResource !== null) {
|
||||
return $anidbResource;
|
||||
}
|
||||
return $this->getAnilistImage($anilistResource);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -56,7 +53,7 @@ class BackfillLargeCoverImage extends BackfillAnimeImage
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getAnidbImage(ExternalResource $anilistResource): ?Image
|
||||
protected function getAnilistImage(ExternalResource $anilistResource): ?Image
|
||||
{
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
+7
-10
@@ -2,21 +2,21 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Image;
|
||||
namespace App\Actions\Models\Wiki\Anime\Image;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeImageAction;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeImage;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Class BackfillSmallCoverImage.
|
||||
* Class BackfillSmallCoverImageAction.
|
||||
*/
|
||||
class BackfillSmallCoverImage extends BackfillAnimeImage
|
||||
class BackfillSmallCoverImageAction extends BackfillAnimeImageAction
|
||||
{
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
@@ -37,12 +37,9 @@ class BackfillSmallCoverImage extends BackfillAnimeImage
|
||||
*/
|
||||
protected function getImage(): ?Image
|
||||
{
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbImage($anilistResource);
|
||||
if ($anidbResource !== null) {
|
||||
return $anidbResource;
|
||||
}
|
||||
return $this->getAnilistImage($anilistResource);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -56,7 +53,7 @@ class BackfillSmallCoverImage extends BackfillAnimeImage
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getAnidbImage(ExternalResource $anilistResource): ?Image
|
||||
protected function getAnilistImage(ExternalResource $anilistResource): ?Image
|
||||
{
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
+7
-7
@@ -2,19 +2,19 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Class BackfillAnidbResource.
|
||||
* Class BackfillAnidbResourceAction.
|
||||
*/
|
||||
class BackfillAnidbResource extends BackfillAnimeResource
|
||||
class BackfillAnidbResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -37,7 +37,7 @@ class BackfillAnidbResource extends BackfillAnimeResource
|
||||
{
|
||||
// Allow fall-throughs in case AniDB Resource is not mapped to every external site.
|
||||
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbMapping($malResource, 'myanimelist');
|
||||
if ($anidbResource !== null) {
|
||||
@@ -48,7 +48,7 @@ class BackfillAnidbResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbMapping($anilistResource, 'anilist');
|
||||
if ($anidbResource !== null) {
|
||||
@@ -59,7 +59,7 @@ class BackfillAnidbResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
return $this->getAnidbMapping($kitsuResource, 'kitsu');
|
||||
}
|
||||
+7
-7
@@ -2,20 +2,20 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnilistResource.
|
||||
* Class BackfillAnilistResourceAction.
|
||||
*/
|
||||
class BackfillAnilistResource extends BackfillAnimeResource
|
||||
class BackfillAnilistResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -38,7 +38,7 @@ class BackfillAnilistResource extends BackfillAnimeResource
|
||||
{
|
||||
// Allow fall-throughs in case Anilist Resource is not mapped to every external site.
|
||||
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$anilistResource = $this->getMalAnilistMapping($malResource);
|
||||
if ($anilistResource !== null) {
|
||||
@@ -46,7 +46,7 @@ class BackfillAnilistResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
$anilistResource = $this->getKitsuAnilistMapping($kitsuResource);
|
||||
if ($anilistResource !== null) {
|
||||
@@ -54,7 +54,7 @@ class BackfillAnilistResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$anidbResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
$anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
if ($anidbResource instanceof ExternalResource) {
|
||||
return $this->getAnidbAnilistMapping($anidbResource);
|
||||
}
|
||||
+5
-5
@@ -2,20 +2,20 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnnResource.
|
||||
* Class BackfillAnnResourceAction.
|
||||
*/
|
||||
class BackfillAnnResource extends BackfillAnimeResource
|
||||
class BackfillAnnResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -36,7 +36,7 @@ class BackfillAnnResource extends BackfillAnimeResource
|
||||
*/
|
||||
protected function getResource(): ?ExternalResource
|
||||
{
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
return $this->getKitsuAnnMapping($kitsuResource);
|
||||
}
|
||||
+8
-8
@@ -2,20 +2,20 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillKitsuResource.
|
||||
* Class BackfillKitsuResourceAction.
|
||||
*/
|
||||
class BackfillKitsuResource extends BackfillAnimeResource
|
||||
class BackfillKitsuResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -38,7 +38,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
{
|
||||
// Allow fall-throughs in case Kitsu Resource is not mapped to every external site.
|
||||
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$kitsuResource = $this->getKitsuMapping($malResource, 'MYANIMELIST_ANIME');
|
||||
if ($kitsuResource !== null) {
|
||||
@@ -49,7 +49,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$kitsuResource = $this->getKitsuMapping($anilistResource, 'ANILIST_ANIME');
|
||||
if ($kitsuResource !== null) {
|
||||
@@ -60,7 +60,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$anidbResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
$anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
if ($anidbResource instanceof ExternalResource) {
|
||||
$kitsuResource = $this->getKitsuMapping($anidbResource, 'ANIDB');
|
||||
if ($kitsuResource !== null) {
|
||||
@@ -71,7 +71,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$annResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN);
|
||||
$annResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN);
|
||||
if ($annResource instanceof ExternalResource) {
|
||||
return $this->getKitsuMapping($annResource, 'ANIMENEWSNETWORK');
|
||||
}
|
||||
+7
-7
@@ -2,20 +2,20 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillMalResource.
|
||||
* Class BackfillMalResourceAction.
|
||||
*/
|
||||
class BackfillMalResource extends BackfillAnimeResource
|
||||
class BackfillMalResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -36,7 +36,7 @@ class BackfillMalResource extends BackfillAnimeResource
|
||||
*/
|
||||
protected function getResource(): ?ExternalResource
|
||||
{
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
$malResource = $this->getKitsuMalMapping($kitsuResource);
|
||||
if ($malResource !== null) {
|
||||
@@ -44,7 +44,7 @@ class BackfillMalResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$malResource = $this->getAnilistMalMapping($anilistResource);
|
||||
if ($malResource !== null) {
|
||||
@@ -52,7 +52,7 @@ class BackfillMalResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$anidbResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
$anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
if ($anidbResource instanceof ExternalResource) {
|
||||
return $this->getAnidbMalMapping($anidbResource);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime\Studio;
|
||||
|
||||
use App\Actions\Models\Wiki\BackfillStudiosAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeStudiosAction.
|
||||
*
|
||||
* @extends BackfillStudiosAction<Anime>
|
||||
*/
|
||||
class BackfillAnimeStudiosAction extends BackfillStudiosAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function __construct(Anime $anime)
|
||||
{
|
||||
parent::__construct($anime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Anime
|
||||
*/
|
||||
protected function getModel(): Anime
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to studios.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->studios();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query third-party API for Anime Studios.
|
||||
*
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getStudios(): array
|
||||
{
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$studios = $this->getMalAnimeStudios($malResource);
|
||||
if (! empty($studios)) {
|
||||
return $studios;
|
||||
}
|
||||
}
|
||||
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$studios = $this->getAnilistAnimeStudios($anilistResource);
|
||||
if (! empty($studios)) {
|
||||
return $studios;
|
||||
}
|
||||
}
|
||||
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
return $this->getKitsuAnimeStudios($kitsuResource);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Query MAL API for Anime Studios.
|
||||
*
|
||||
* @param ExternalResource $malResource
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getMalAnimeStudios(ExternalResource $malResource): array
|
||||
{
|
||||
$studios = [];
|
||||
|
||||
$response = Http::withHeaders(['X-MAL-CLIENT-ID' => Config::get('services.mal.client')])
|
||||
->get("https://api.myanimelist.net/v2/anime/$malResource->external_id", [
|
||||
'fields' => 'studios',
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
$malStudios = Arr::get($response, 'studios', []);
|
||||
|
||||
foreach ($malStudios as $malStudio) {
|
||||
$name = Arr::get($malStudio, 'name');
|
||||
$id = Arr::get($malStudio, 'id');
|
||||
if (empty($name) || empty($id)) {
|
||||
Log::info("Skipping empty studio of name '$name' and id '$id' for MAL Resource '{$malResource->getName()}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
$studio = $this->getOrCreateStudio($name);
|
||||
|
||||
$studios[] = $studio;
|
||||
|
||||
$this->ensureStudioHasResource($studio, ResourceSite::MAL(), $id);
|
||||
}
|
||||
|
||||
return $studios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Anilist API for Anime Studios.
|
||||
*
|
||||
* @param ExternalResource $anilistResource
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getAnilistAnimeStudios(ExternalResource $anilistResource): array
|
||||
{
|
||||
$studios = [];
|
||||
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
Media (id: $id, type: ANIME) {
|
||||
studios (isMain: true) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'id' => $anilistResource->external_id,
|
||||
];
|
||||
|
||||
$response = Http::post('https://graphql.anilist.co', [
|
||||
'query' => $query,
|
||||
'variables' => $variables,
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
$anilistStudios = Arr::get($response, 'data.Media.studios.nodes', []);
|
||||
|
||||
foreach ($anilistStudios as $anilistStudio) {
|
||||
$name = Arr::get($anilistStudio, 'name');
|
||||
$id = Arr::get($anilistStudio, 'id');
|
||||
if (empty($name) || empty($id)) {
|
||||
Log::info("Skipping empty studio of name '$name' and id '$id' for Anilist Resource '{$anilistResource->getName()}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
$studio = $this->getOrCreateStudio($name);
|
||||
|
||||
$studios[] = $studio;
|
||||
|
||||
$this->ensureStudioHasResource($studio, ResourceSite::ANILIST(), $id);
|
||||
}
|
||||
|
||||
return $studios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Kitsu API for Anime Studios.
|
||||
*
|
||||
* @param ExternalResource $kitsuResource
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getKitsuAnimeStudios(ExternalResource $kitsuResource): array
|
||||
{
|
||||
$studios = [];
|
||||
|
||||
$query = '
|
||||
query ($id: ID!) {
|
||||
findAnimeById(id: $id) {
|
||||
productions(first:20) {
|
||||
nodes {
|
||||
role
|
||||
company {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'id' => $kitsuResource->external_id,
|
||||
];
|
||||
|
||||
$response = Http::post('https://kitsu.io/api/graphql', [
|
||||
'query' => $query,
|
||||
'variables' => $variables,
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
$kitsuStudios = Arr::get($response, 'data.findAnimeById.productions.nodes', []);
|
||||
|
||||
foreach ($kitsuStudios as $kitsuStudio) {
|
||||
$role = Arr::get($kitsuStudio, 'role');
|
||||
$name = Arr::get($kitsuStudio, 'company.name');
|
||||
if ($role !== 'STUDIO' || empty($name)) {
|
||||
Log::info("Skipping production company of name '$name' and role '$role' for Anilist Resource '{$kitsuResource->getName()}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
$studio = $this->getOrCreateStudio($name);
|
||||
|
||||
$studios[] = $studio;
|
||||
}
|
||||
|
||||
return $studios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Studios.
|
||||
*
|
||||
* @param Studio[] $studios
|
||||
* @return void
|
||||
*/
|
||||
protected function attachStudios(array $studios): void
|
||||
{
|
||||
Log::info("Attaching studios to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach(Arr::pluck($studios, Studio::ATTRIBUTE_ID));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BaseAction;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Models\Wiki\Image;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Testing\File;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class BackfillImageAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
* @extends BaseAction<TModel>
|
||||
*/
|
||||
abstract class BackfillImageAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
if ($this->relation()->getQuery()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->exists()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Image of Facet '{$this->getFacet()->value}'.");
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED());
|
||||
}
|
||||
|
||||
$image = $this->getImage();
|
||||
|
||||
if ($image !== null) {
|
||||
$this->attachImage($image);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->doesntExist()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no {$this->getFacet()->description} Image after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Image from response.
|
||||
*
|
||||
* @param string $url
|
||||
* @return Image
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function createImage(string $url): Image
|
||||
{
|
||||
$imageResponse = Http::get($url)->throw();
|
||||
|
||||
$image = $imageResponse->body();
|
||||
|
||||
$file = File::createWithContent(basename($url), $image);
|
||||
|
||||
$fs = Storage::disk(Config::get('image.disk'));
|
||||
|
||||
$fsFile = $fs->putFile($this->path(), $file);
|
||||
|
||||
/** @var Image $image */
|
||||
$image = Image::query()->create([
|
||||
Image::ATTRIBUTE_FACET => $this->getFacet()->value,
|
||||
Image::ATTRIBUTE_PATH => $fsFile,
|
||||
]);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to storage image in filesystem.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function path(): string
|
||||
{
|
||||
return Str::of(Str::kebab(class_basename($this->getModel())))
|
||||
->append(DIRECTORY_SEPARATOR)
|
||||
->append(Str::kebab($this->getFacet()->description))
|
||||
->__toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Image to model.
|
||||
*
|
||||
* @param Image $image
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function attachImage(Image $image): void;
|
||||
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
*
|
||||
* @return ImageFacet
|
||||
*/
|
||||
abstract protected function getFacet(): ImageFacet;
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Image.
|
||||
*
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getImage(): ?Image;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BaseAction;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillResourceAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
* @extends BaseAction<TModel>
|
||||
*/
|
||||
abstract class BackfillResourceAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
if ($this->relation()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->exists()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Resource of Site '{$this->getSite()->value}'.");
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED());
|
||||
}
|
||||
|
||||
$resource = $this->getResource();
|
||||
|
||||
if ($resource !== null) {
|
||||
$this->attachResource($resource);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->doesntExist()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no {$this->getSite()->description} Resource after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or Create Resource from response.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string|null $slug
|
||||
* @return ExternalResource
|
||||
*/
|
||||
abstract protected function getOrCreateResource(int $id, string $slug = null): ExternalResource;
|
||||
|
||||
/**
|
||||
* Attach External Resource to model.
|
||||
*
|
||||
* @param ExternalResource $resource
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function attachResource(ExternalResource $resource): void;
|
||||
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
*
|
||||
* @return ResourceSite
|
||||
*/
|
||||
abstract protected function getSite(): ResourceSite;
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Resource mapping.
|
||||
*
|
||||
* @return ExternalResource|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getResource(): ?ExternalResource;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BaseAction;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\StudioResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class BackfillStudiosAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
* @extends BaseAction<TModel>
|
||||
*/
|
||||
abstract class BackfillStudiosAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
if ($this->relation()->getQuery()->exists()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Studios.");
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED());
|
||||
}
|
||||
|
||||
$studios = $this->getStudios();
|
||||
|
||||
if (! empty($studios)) {
|
||||
$this->attachStudios($studios);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->doesntExist()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no Studios after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create Studio from name (case-insensitive).
|
||||
*
|
||||
* @param string $name
|
||||
* @return Studio
|
||||
*/
|
||||
protected function getOrCreateStudio(string $name): Studio
|
||||
{
|
||||
$column = Studio::ATTRIBUTE_NAME;
|
||||
$studio = Studio::query()
|
||||
->where(DB::raw("lower($column)"), Str::lower($name))
|
||||
->first();
|
||||
|
||||
if (! $studio instanceof Studio) {
|
||||
Log::info("Creating studio '$name'");
|
||||
|
||||
$studio = Studio::query()->create([
|
||||
Studio::ATTRIBUTE_NAME => $name,
|
||||
Studio::ATTRIBUTE_SLUG => Str::slug($name, '_'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $studio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure Studio has Resource.
|
||||
*
|
||||
* @param Studio $studio
|
||||
* @param ResourceSite $site
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
protected function ensureStudioHasResource(Studio $studio, ResourceSite $site, int $id): void
|
||||
{
|
||||
$studioResource = ExternalResource::query()
|
||||
->where(ExternalResource::ATTRIBUTE_SITE, $site->value)
|
||||
->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id)
|
||||
->where(ExternalResource::ATTRIBUTE_LINK, ResourceSite::formatStudioResourceLink($site, $id))
|
||||
->first();
|
||||
|
||||
if (! $studioResource instanceof ExternalResource) {
|
||||
Log::info("Creating studio resource with site '$site->value' and id '$id'");
|
||||
|
||||
$studioResource = ExternalResource::query()->create([
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id,
|
||||
ExternalResource::ATTRIBUTE_LINK => ResourceSite::formatStudioResourceLink($site, $id),
|
||||
ExternalResource::ATTRIBUTE_SITE => $site->value,
|
||||
]);
|
||||
}
|
||||
|
||||
if (StudioResource::query()
|
||||
->where($studio->getKeyName(), $studio->getKey())
|
||||
->where($studioResource->getKeyName(), $studioResource->getKey())
|
||||
->doesntExist()
|
||||
) {
|
||||
Log::info("Attaching resource '$studioResource->link' to studio '{$studio->getName()}'");
|
||||
$studioResource->studios()->attach($studio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Studios.
|
||||
*
|
||||
* @param Studio[] $studios
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function attachStudios(array $studios): void;
|
||||
|
||||
/**
|
||||
* Query third-party API for Studios.
|
||||
*
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getStudios(): array;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Studio;
|
||||
|
||||
use App\Actions\Models\Wiki\BackfillImageAction;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillStudioImageAction.
|
||||
*
|
||||
* @extends BackfillImageAction<Studio>
|
||||
*/
|
||||
abstract class BackfillStudioImageAction extends BackfillImageAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Studio $studio
|
||||
*/
|
||||
public function __construct(Studio $studio)
|
||||
{
|
||||
parent::__construct($studio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Studio
|
||||
*/
|
||||
protected function getModel(): Studio
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to images.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->images();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Image to Studio.
|
||||
*
|
||||
* @param Image $image
|
||||
* @return void
|
||||
*/
|
||||
protected function attachImage(Image $image): void
|
||||
{
|
||||
Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach($image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Studio\Image;
|
||||
|
||||
use App\Actions\Models\Wiki\Studio\BackfillStudioImageAction;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
|
||||
/**
|
||||
* Class BackfillLargeCoverImageAction.
|
||||
*/
|
||||
class BackfillLargeCoverImageAction extends BackfillStudioImageAction
|
||||
{
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
*
|
||||
* @return ImageFacet
|
||||
*/
|
||||
protected function getFacet(): ImageFacet
|
||||
{
|
||||
return ImageFacet::COVER_LARGE();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Image.
|
||||
*
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getImage(): ?Image
|
||||
{
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
return $this->getMalImage($malResource);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query MAL API for large cover image.
|
||||
*
|
||||
* @param ExternalResource $malResource
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getMalImage(ExternalResource $malResource): ?Image
|
||||
{
|
||||
return $this->createImage("https://cdn.myanimelist.net/img/common/companies/$malResource->external_id.png");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Video\Audio;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BaseAction;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositories;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Actions\Models\Wiki\Video\DeriveSourceVideo;
|
||||
use App\Enums\Actions\Models\Wiki\Video\OverwriteAudio;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Audio;
|
||||
use App\Models\Wiki\Video;
|
||||
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
|
||||
use App\Repositories\Storage\Wiki\AudioRepository as AudioSourceRepository;
|
||||
use Exception;
|
||||
use FFMpeg\Coordinate\TimeCode;
|
||||
use FFMpeg\Filters\Audio\AddMetadataFilter;
|
||||
use FFMpeg\Filters\Audio\AudioClipFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use ProtoneMedia\LaravelFFMpeg\Support\FFMpeg;
|
||||
|
||||
/**
|
||||
* Class BackfillVideoAudioAction.
|
||||
*
|
||||
* @extends BaseAction<Video>
|
||||
*/
|
||||
class BackfillAudioAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Video $video
|
||||
* @param DeriveSourceVideo $deriveSourceVideo
|
||||
* @param OverwriteAudio $overwriteAudio
|
||||
*/
|
||||
public function __construct(
|
||||
Video $video,
|
||||
protected readonly DeriveSourceVideo $deriveSourceVideo = new DeriveSourceVideo(DeriveSourceVideo::YES),
|
||||
protected readonly OverwriteAudio $overwriteAudio = new OverwriteAudio(OverwriteAudio::NO)
|
||||
) {
|
||||
parent::__construct($video);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
if ($this->relation()->getQuery()->exists() && ! $this->overwriteAudio()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Audio'.");
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED());
|
||||
}
|
||||
|
||||
$audio = $this->getAudio();
|
||||
|
||||
if ($audio !== null) {
|
||||
$this->attachAudio($audio);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->doesntExist()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no Audio after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Video
|
||||
*/
|
||||
protected function getModel(): Video
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to audio.
|
||||
*
|
||||
* @return BelongsTo
|
||||
*/
|
||||
protected function relation(): BelongsTo
|
||||
{
|
||||
return $this->getModel()->audio();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the source video should be derived.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function deriveSourceVideo(): bool
|
||||
{
|
||||
return DeriveSourceVideo::YES()->is($this->deriveSourceVideo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if audio should be overwritten.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function overwriteAudio(): bool
|
||||
{
|
||||
return OverwriteAudio::YES()->is($this->overwriteAudio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or Create Audio.
|
||||
*
|
||||
* @return Audio|null
|
||||
*/
|
||||
protected function getAudio(): ?Audio
|
||||
{
|
||||
// Allow bypassing of source video derivation
|
||||
$sourceVideo = $this->deriveSourceVideo()
|
||||
? $this->getSourceVideo()
|
||||
: $this->getModel();
|
||||
|
||||
// It's possible that the video is not attached to any themes, exit early.
|
||||
if ($sourceVideo === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First, attempt to set audio from the source video
|
||||
$audio = $sourceVideo->audio;
|
||||
|
||||
// Anticipate audio path for FFmpeg save file
|
||||
$audioPath = $audio === null
|
||||
? Str::replace('webm', 'ogg', $sourceVideo->path)
|
||||
: $audio->path;
|
||||
|
||||
// Second, attempt to set audio from path
|
||||
if ($audio === null) {
|
||||
$audio = Audio::query()->firstWhere(Audio::ATTRIBUTE_PATH, $audioPath);
|
||||
}
|
||||
|
||||
// Finally, extract audio from the source video
|
||||
if ($audio === null || $this->overwriteAudio()) {
|
||||
Log::info("Extracting Audio from Video '{$sourceVideo->getName()}'");
|
||||
|
||||
$this->extractAudio($sourceVideo, $audioPath);
|
||||
$results = $this->reconcileAudio($audioPath);
|
||||
$results->toLog();
|
||||
|
||||
if ($audio === null) {
|
||||
$audio = $results->getCreated()->firstWhere(fn (Audio $audio) => $audio->path === $audioPath);
|
||||
}
|
||||
}
|
||||
|
||||
return $audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source video for the given video.
|
||||
*
|
||||
* @return Video|null
|
||||
*/
|
||||
protected function getSourceVideo(): ?Video
|
||||
{
|
||||
$source = null;
|
||||
|
||||
$sourceCandidates = $this->getAdjacentVideos();
|
||||
|
||||
foreach ($sourceCandidates as $sourceCandidate) {
|
||||
if (! $source instanceof Video || $sourceCandidate->getSourcePriority() > $source->getSourcePriority()) {
|
||||
$source = $sourceCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the adjacent videos for sourcing.
|
||||
*
|
||||
* @return Collection<int, Video>
|
||||
*/
|
||||
protected function getAdjacentVideos(): Collection
|
||||
{
|
||||
$builder = AnimeTheme::query();
|
||||
|
||||
$sortRelation = $builder->getRelation(AnimeTheme::RELATION_ANIME);
|
||||
|
||||
$orderByNameQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_NAME]);
|
||||
$orderBySeasonQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_SEASON]);
|
||||
$orderByYearQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_YEAR]);
|
||||
|
||||
return $builder->whereHas(AnimeTheme::RELATION_VIDEOS, fn (Builder $relationBuilder) => $relationBuilder->whereKey($this->getModel()))
|
||||
->orderBy($orderByYearQuery->toBase())
|
||||
->orderBy($orderBySeasonQuery->toBase())
|
||||
->orderBy($orderByNameQuery->toBase())
|
||||
->with([
|
||||
AnimeTheme::RELATION_ANIME,
|
||||
AnimeTheme::RELATION_AUDIO,
|
||||
AnimeTheme::RELATION_ENTRIES => fn (Relation $relation) => $relation->getQuery()->orderBy(AnimeThemeEntry::ATTRIBUTE_VERSION),
|
||||
])
|
||||
->get()
|
||||
->flatMap(fn (AnimeTheme $theme) => $theme->animethemeentries)
|
||||
->flatMap(fn (AnimeThemeEntry $entry) => $entry->videos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract audio stream from video and store in filesystem.
|
||||
*
|
||||
* @param Video $video
|
||||
* @param string $audioPath
|
||||
* @return void
|
||||
*/
|
||||
protected function extractAudio(Video $video, string $audioPath): void
|
||||
{
|
||||
try {
|
||||
foreach (Config::get(AudioConstants::DISKS_QUALIFIED, []) as $audioDisk) {
|
||||
FFMpeg::fromDisk(Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED))
|
||||
->open($video->path)
|
||||
->addFilter(new AudioClipFilter(new TimeCode(0, 0, 0, 0)))
|
||||
->addFilter(new AddMetadataFilter())
|
||||
->export()
|
||||
->toDisk($audioDisk)
|
||||
->save($audioPath);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
} finally {
|
||||
FFMpeg::cleanupTemporaryFiles();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile audio repositories.
|
||||
*
|
||||
* @param string $audioPath
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
protected function reconcileAudio(string $audioPath): ReconcileResults
|
||||
{
|
||||
$action = new ReconcileAudioRepositories();
|
||||
|
||||
/** @var RepositoryInterface $sourceRepository */
|
||||
$sourceRepository = App::make(AudioSourceRepository::class);
|
||||
$sourceRepository->handleFilter('path', File::dirname($audioPath));
|
||||
|
||||
/** @var RepositoryInterface $destinationRepository */
|
||||
$destinationRepository = App::make(AudioDestinationRepository::class);
|
||||
$destinationRepository->handleFilter('path', File::dirname($audioPath));
|
||||
|
||||
return $action->reconcileRepositories($sourceRepository, $destinationRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Audio to model.
|
||||
*
|
||||
* @param Audio $audio
|
||||
* @return void
|
||||
*/
|
||||
protected function attachAudio(Audio $audio): void
|
||||
{
|
||||
if ($this->relation()->isNot($audio)) {
|
||||
Log::info("Associating Audio '{$audio->getName()}' with Video '{$this->getModel()->getName()}'");
|
||||
$this->relation()->associate($audio)->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-6
@@ -2,9 +2,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns\Repositories\Billing;
|
||||
namespace App\Actions\Repositories\Billing\Balance;
|
||||
|
||||
use App\Concerns\Repositories\ReconcilesRepositories;
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Enums\Http\Api\Filter\AllowedDateFormat;
|
||||
use App\Models\Billing\Balance;
|
||||
use Closure;
|
||||
@@ -12,12 +13,12 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Trait ReconcilesBalanceRepositories.
|
||||
* Class ReconcileBalanceRepositories.
|
||||
*
|
||||
* @extends ReconcileRepositories<Balance>
|
||||
*/
|
||||
trait ReconcilesBalanceRepositories
|
||||
class ReconcileBalanceRepositories extends ReconcileRepositories
|
||||
{
|
||||
use ReconcilesRepositories;
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
@@ -52,6 +53,7 @@ trait ReconcilesBalanceRepositories
|
||||
return [
|
||||
Balance::ATTRIBUTE_BALANCE,
|
||||
Balance::ATTRIBUTE_DATE,
|
||||
Balance::ATTRIBUTE_FREQUENCY,
|
||||
Balance::ATTRIBUTE_ID,
|
||||
Balance::ATTRIBUTE_SERVICE,
|
||||
Balance::ATTRIBUTE_USAGE,
|
||||
@@ -84,4 +86,17 @@ trait ReconcilesBalanceRepositories
|
||||
fn (Balance $balance) => $balance->date->format(AllowedDateFormat::YM) === $formattedDestinationDate
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*
|
||||
* @param Collection $created
|
||||
* @param Collection $deleted
|
||||
* @param Collection $updated
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
|
||||
{
|
||||
return new ReconcileBalanceResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Billing\Balance;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Billing\Balance;
|
||||
|
||||
/**
|
||||
* Class ReconcileBalanceResults.
|
||||
*
|
||||
* @extends ReconcileResults<Balance>
|
||||
*/
|
||||
class ReconcileBalanceResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Balance>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Balance::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Billing\Transaction;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Enums\Http\Api\Filter\AllowedDateFormat;
|
||||
use App\Models\Billing\Transaction;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class ReconcileTransactionRepositories.
|
||||
*
|
||||
* @extends ReconcileRepositories<Transaction>
|
||||
*/
|
||||
class ReconcileTransactionRepositories extends ReconcileRepositories
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Transaction::ATTRIBUTE_AMOUNT,
|
||||
Transaction::ATTRIBUTE_DATE,
|
||||
Transaction::ATTRIBUTE_EXTERNAL_ID,
|
||||
Transaction::ATTRIBUTE_ID,
|
||||
Transaction::ATTRIBUTE_SERVICE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Transaction $first, Transaction $second) => [$first->external_id, $first->date->format(AllowedDateFormat::YMD), $first->amount]
|
||||
<=> [$second->external_id, $second->date->format(AllowedDateFormat::YMD), $second->amount];
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn () => 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*
|
||||
* @param Collection $sourceModels
|
||||
* @param Model $destinationModel
|
||||
* @return Model|null
|
||||
*/
|
||||
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*
|
||||
* @param Collection $created
|
||||
* @param Collection $deleted
|
||||
* @param Collection $updated
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
|
||||
{
|
||||
return new ReconcileTransactionResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Billing\Transaction;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Billing\Transaction;
|
||||
|
||||
/**
|
||||
* Class ReconcileTransactionResults.
|
||||
*
|
||||
* @extends ReconcileResults<Transaction>
|
||||
*/
|
||||
class ReconcileTransactionResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Transaction>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Transaction::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories;
|
||||
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class ReconcileRepositories.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class ReconcileRepositories
|
||||
{
|
||||
/**
|
||||
* Perform set reconciliation between source and destination repositories.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $source
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
public function reconcileRepositories(RepositoryInterface $source, RepositoryInterface $destination): ReconcileResults
|
||||
{
|
||||
$sourceModels = $source->get();
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForCreateDelete());
|
||||
|
||||
$created = $this->createModelsFromSource($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$deleted = $this->deleteModelsFromDestination($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForUpdate());
|
||||
|
||||
$updated = $this->updateDestinationModels($destination, $sourceModels, $destinationModels);
|
||||
|
||||
return $this->getResults($created, $deleted, $updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
abstract protected function columnsForCreateDelete(): array;
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
abstract protected function diffCallbackForCreateDelete(): Closure;
|
||||
|
||||
/**
|
||||
* Create models that exist in source but not in destination.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return Collection
|
||||
*/
|
||||
protected function createModelsFromSource(
|
||||
RepositoryInterface $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): Collection {
|
||||
$createModels = $sourceModels->diffUsing($destinationModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
return $createModels->each(fn (Model $createModel) => $destination->save($createModel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete models that exist in destination but not in source.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return Collection
|
||||
*/
|
||||
protected function deleteModelsFromDestination(
|
||||
RepositoryInterface $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): Collection {
|
||||
$deleteModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
return $deleteModels->each(fn (Model $deleteModel) => $destination->delete($deleteModel));
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
abstract protected function columnsForUpdate(): array;
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
abstract protected function diffCallbackForUpdate(): Closure;
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*
|
||||
* @param Collection $sourceModels
|
||||
* @param Model $destinationModel
|
||||
* @return Model|null
|
||||
*/
|
||||
abstract protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model;
|
||||
|
||||
/**
|
||||
* Update destination models that have changed in source.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return Collection
|
||||
*/
|
||||
protected function updateDestinationModels(
|
||||
RepositoryInterface $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): Collection {
|
||||
$updatedModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForUpdate());
|
||||
|
||||
return $updatedModels->each(function (Model $updatedModel) use ($sourceModels, $destination) {
|
||||
$sourceModel = $this->resolveUpdatedModel($sourceModels, $updatedModel);
|
||||
if ($sourceModel !== null) {
|
||||
$destination->update($updatedModel, $sourceModel->toArray());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*
|
||||
* @param Collection $created
|
||||
* @param Collection $deleted
|
||||
* @param Collection $updated
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
abstract protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Countable;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class ReconcileResults.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Create a new results instance.
|
||||
*
|
||||
* @param Collection $created
|
||||
* @param Collection $deleted
|
||||
* @param Collection $updated
|
||||
*/
|
||||
public function __construct(
|
||||
protected readonly Collection $created,
|
||||
protected readonly Collection $deleted,
|
||||
protected readonly Collection $updated
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get created models.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getCreated(): Collection
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any successful changes were made during reconciliation.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasChanges(): bool
|
||||
{
|
||||
return $this->created->isNotEmpty() || $this->deleted->isNotEmpty() || $this->updated->isNotEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write reconcile results to log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
$this->created->each(fn (BaseModel $model) => Log::info("{$this->label()} '{$model->getName()}' created"));
|
||||
$this->deleted->each(fn (BaseModel $model) => Log::info("{$this->label()} '{$model->getName()}' deleted"));
|
||||
$this->updated->each(fn (BaseModel $model) => Log::info("{$this->label()} '{$model->getName()}' updated"));
|
||||
|
||||
Log::info($this->summary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Write reconcile results to console output.
|
||||
*
|
||||
* @param Command $command
|
||||
* @return void
|
||||
*/
|
||||
public function toConsole(Command $command): void
|
||||
{
|
||||
$this->created->each(fn (BaseModel $model) => $command->info("{$this->label()} '{$model->getName()}' created"));
|
||||
$this->deleted->each(fn (BaseModel $model) => $command->info("{$this->label()} '{$model->getName()}' deleted"));
|
||||
$this->updated->each(fn (BaseModel $model) => $command->info("{$this->label()} '{$model->getName()}' updated"));
|
||||
|
||||
$command->info($this->summary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the summary line.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function summary(): string
|
||||
{
|
||||
if ($this->hasChanges()) {
|
||||
return "{$this->created->count()} {$this->label($this->created)} created, {$this->deleted->count()} {$this->label($this->deleted)} deleted, {$this->updated->count()} {$this->label($this->updated)} updated";
|
||||
}
|
||||
|
||||
return "No {$this->label($this->created)} created or deleted or updated";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<TModel>
|
||||
*/
|
||||
abstract protected function model(): string;
|
||||
|
||||
/**
|
||||
* Get the user-friendly label for the model class name.
|
||||
*
|
||||
* @param int|array|Countable $models
|
||||
* @return string
|
||||
*/
|
||||
protected function label(int|array|Countable $models = 1): string
|
||||
{
|
||||
return Str::plural(class_basename($this->model()), $models);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Audio;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class ReconcileAudioRepositories.
|
||||
*
|
||||
* @extends ReconcileRepositories<Audio>
|
||||
*/
|
||||
class ReconcileAudioRepositories extends ReconcileRepositories
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Audio::ATTRIBUTE_BASENAME,
|
||||
Audio::ATTRIBUTE_ID,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Audio $first, Audio $second) => $first->basename <=> $second->basename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return [
|
||||
Audio::ATTRIBUTE_BASENAME,
|
||||
Audio::ATTRIBUTE_FILENAME,
|
||||
Audio::ATTRIBUTE_ID,
|
||||
Audio::ATTRIBUTE_MIMETYPE,
|
||||
Audio::ATTRIBUTE_PATH,
|
||||
Audio::ATTRIBUTE_SIZE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (Audio $first, Audio $second) => [$first->basename, $first->path, $first->size] <=> [$second->basename, $second->path, $second->size];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*
|
||||
* @param Collection $sourceModels
|
||||
* @param Model $destinationModel
|
||||
* @return Model|null
|
||||
*/
|
||||
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
|
||||
{
|
||||
return $sourceModels->firstWhere(
|
||||
Audio::ATTRIBUTE_BASENAME,
|
||||
$destinationModel->getAttribute(Audio::ATTRIBUTE_BASENAME)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*
|
||||
* @param Collection $created
|
||||
* @param Collection $deleted
|
||||
* @param Collection $updated
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
|
||||
{
|
||||
return new ReconcileAudioResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Audio;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Audio;
|
||||
|
||||
/**
|
||||
* Class ReconcileAudioResults.
|
||||
*
|
||||
* @extends ReconcileResults<Audio>
|
||||
*/
|
||||
class ReconcileAudioResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Audio>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Audio::class;
|
||||
}
|
||||
}
|
||||
+22
-6
@@ -2,21 +2,22 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns\Repositories\Wiki;
|
||||
namespace App\Actions\Repositories\Wiki\Video;
|
||||
|
||||
use App\Concerns\Repositories\ReconcilesRepositories;
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Video;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Trait ReconcilesVideoRepositories.
|
||||
* Class ReconcileVideoRepositories.
|
||||
*
|
||||
* @extends ReconcileRepositories<Video>
|
||||
*/
|
||||
trait ReconcilesVideoRepositories
|
||||
class ReconcileVideoRepositories extends ReconcileRepositories
|
||||
{
|
||||
use ReconcilesRepositories;
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
@@ -49,7 +50,9 @@ trait ReconcilesVideoRepositories
|
||||
{
|
||||
return [
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
Video::ATTRIBUTE_FILENAME,
|
||||
Video::ATTRIBUTE_ID,
|
||||
Video::ATTRIBUTE_MIMETYPE,
|
||||
Video::ATTRIBUTE_PATH,
|
||||
Video::ATTRIBUTE_SIZE,
|
||||
];
|
||||
@@ -79,4 +82,17 @@ trait ReconcilesVideoRepositories
|
||||
$destinationModel->getAttribute(Video::ATTRIBUTE_BASENAME)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*
|
||||
* @param Collection $created
|
||||
* @param Collection $deleted
|
||||
* @param Collection $updated
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
|
||||
{
|
||||
return new ReconcileVideoResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Video;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Video;
|
||||
|
||||
/**
|
||||
* Class ReconcileVideoResults.
|
||||
*
|
||||
* @extends ReconcileResults<Video>
|
||||
*/
|
||||
class ReconcileVideoResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Video>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Video::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Base;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Actions\Storage\StorageAction;
|
||||
use App\Actions\Storage\StorageResults;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Class DeleteAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class DeleteAction extends StorageAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param TModel $model
|
||||
*/
|
||||
public function __construct(protected readonly BaseModel $model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return StorageResults
|
||||
*/
|
||||
public function handle(): StorageResults
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($this->disks() as $disk) {
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk($disk);
|
||||
|
||||
$result = $fs->delete($this->path());
|
||||
|
||||
$results[$disk] = $result;
|
||||
}
|
||||
|
||||
$reconcileResults = $this->reconcileRepositories();
|
||||
|
||||
$reconcileResults->toLog();
|
||||
|
||||
return new DeleteResults($this->model, $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile storage repositories.
|
||||
*
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
protected function reconcileRepositories(): ReconcileResults
|
||||
{
|
||||
$action = $this->action();
|
||||
|
||||
$sourceRepository = $this->getSourceRepository();
|
||||
$destinationRepository = $this->getDestinationRepository();
|
||||
|
||||
$this->handleFilters($sourceRepository, $destinationRepository);
|
||||
|
||||
return $action->reconcileRepositories($sourceRepository, $destinationRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
abstract protected function getSourceRepository(): RepositoryInterface;
|
||||
|
||||
/**
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
abstract protected function getDestinationRepository(): RepositoryInterface;
|
||||
|
||||
/**
|
||||
* Apply filters to repositories before reconciliation.
|
||||
*
|
||||
* @param RepositoryInterface $sourceRepository
|
||||
* @param RepositoryInterface $destinationRepository
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFilters(
|
||||
RepositoryInterface $sourceRepository,
|
||||
RepositoryInterface $destinationRepository
|
||||
): void {
|
||||
$sourceRepository->handleFilter('path', File::dirname($this->path()));
|
||||
$destinationRepository->handleFilter('path', File::dirname($this->path()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reconcile action.
|
||||
*
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
abstract protected function action(): ReconcileRepositories;
|
||||
|
||||
/**
|
||||
* Get the path to delete.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function path(): string;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Base;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Storage\StorageResults;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class DeleteResults.
|
||||
*/
|
||||
class DeleteResults extends StorageResults
|
||||
{
|
||||
/**
|
||||
* Create a new action result instance.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @param array<string, bool> $deletions
|
||||
*/
|
||||
public function __construct(protected readonly BaseModel $model, protected readonly array $deletions = [])
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Write results to log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
if (empty($this->deletions)) {
|
||||
Log::error('No deletions were attempted.');
|
||||
}
|
||||
foreach ($this->deletions as $fs => $result) {
|
||||
$result
|
||||
? Log::info("Deleted '{$this->model->getName()}' from disk '$fs'")
|
||||
: Log::error("Failed to delete '{$this->model->getName()}' from disk '$fs'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to Action Result.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function toActionResult(): ActionResult
|
||||
{
|
||||
if (empty($this->deletions)) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
'No deletions were attempted. Please check that disks are configured.'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var Collection $passed */
|
||||
/** @var Collection $failed */
|
||||
[$passed, $failed] = collect($this->deletions)->partition(fn (bool $result, string $fs) => $result);
|
||||
|
||||
if ($failed->isNotEmpty()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"Failed to delete '{$this->model->getName()}' from disks {$failed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED(),
|
||||
"Deleted '{$this->model->getName()}' from disks {$passed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Base;
|
||||
|
||||
use App\Actions\Storage\StorageAction;
|
||||
use App\Actions\Storage\StorageResults;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Class MoveAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class MoveAction extends StorageAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param TModel $model
|
||||
*/
|
||||
public function __construct(protected BaseModel $model, protected readonly string $to)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return StorageResults
|
||||
*/
|
||||
public function handle(): StorageResults
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$from = $this->from();
|
||||
|
||||
foreach ($this->disks() as $disk) {
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk($disk);
|
||||
|
||||
$result = $fs->move($from, $this->to);
|
||||
|
||||
$results[$disk] = $result;
|
||||
}
|
||||
|
||||
$this->update();
|
||||
|
||||
return new MoveResults($this->model, $from, $this->to, $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to move from.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function from(): string;
|
||||
|
||||
/**
|
||||
* Update underlying model.
|
||||
* We want to apply these updates through Eloquent to preserve relations when renaming.
|
||||
* Otherwise, reconciliation would destroy the old model and create a new model for the new name.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function update(): void;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Base;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Storage\StorageResults;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class MoveResults.
|
||||
*/
|
||||
class MoveResults extends StorageResults
|
||||
{
|
||||
/**
|
||||
* Create a new action result instance.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param array<string, bool> $moves
|
||||
*/
|
||||
public function __construct(
|
||||
protected readonly BaseModel $model,
|
||||
protected readonly string $from,
|
||||
protected readonly string $to,
|
||||
protected readonly array $moves = []
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write results to log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
if (empty($this->moves)) {
|
||||
Log::error('No moves were attempted.');
|
||||
}
|
||||
foreach ($this->moves as $fs => $result) {
|
||||
$result
|
||||
? Log::info("Moved '{$this->model->getName()}' from '$this->from' to '$this->to' in disk '$fs'")
|
||||
: Log::error("Failed to move '{$this->model->getName()}' from '$this->from' to '$this->to' in disk '$fs'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to Action Result.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function toActionResult(): ActionResult
|
||||
{
|
||||
if (empty($this->moves)) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
'No moves were attempted. Please check that disks are configured.'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var Collection $passed */
|
||||
/** @var Collection $failed */
|
||||
[$passed, $failed] = collect($this->moves)->partition(fn (bool $result, string $fs) => $result);
|
||||
|
||||
if ($failed->isNotEmpty()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"Failed to move '{$this->model->getName()}' from '$this->from' to '$this->to' in disks {$failed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED(),
|
||||
"Moved '{$this->model->getName()}' from '$this->from' to '$this->to' in disks {$passed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Base;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Actions\Storage\StorageAction;
|
||||
use App\Actions\Storage\StorageResults;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Class UploadAction.
|
||||
*/
|
||||
abstract class UploadAction extends StorageAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @param string $path
|
||||
*/
|
||||
public function __construct(protected readonly UploadedFile $file, protected readonly string $path)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return StorageResults
|
||||
*/
|
||||
public function handle(): StorageResults
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($this->disks() as $disk) {
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk($disk);
|
||||
|
||||
$result = $fs->putFileAs($this->path, $this->file, $this->file->getClientOriginalName());
|
||||
|
||||
$results[$disk] = $result;
|
||||
}
|
||||
|
||||
$reconcileResults = $this->reconcileRepositories();
|
||||
|
||||
$reconcileResults->toLog();
|
||||
|
||||
return new UploadResults($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile storage repositories.
|
||||
*
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
protected function reconcileRepositories(): ReconcileResults
|
||||
{
|
||||
$action = $this->action();
|
||||
|
||||
$sourceRepository = $this->getSourceRepository();
|
||||
$destinationRepository = $this->getDestinationRepository();
|
||||
|
||||
$this->handleFilters($sourceRepository, $destinationRepository);
|
||||
|
||||
return $action->reconcileRepositories($sourceRepository, $destinationRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
abstract protected function getSourceRepository(): RepositoryInterface;
|
||||
|
||||
/**
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
abstract protected function getDestinationRepository(): RepositoryInterface;
|
||||
|
||||
/**
|
||||
* Apply filters to repositories before reconciliation.
|
||||
*
|
||||
* @param RepositoryInterface $sourceRepository
|
||||
* @param RepositoryInterface $destinationRepository
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFilters(
|
||||
RepositoryInterface $sourceRepository,
|
||||
RepositoryInterface $destinationRepository
|
||||
): void {
|
||||
$sourceRepository->handleFilter('path', $this->path);
|
||||
$destinationRepository->handleFilter('path', $this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reconcile action.
|
||||
*
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
abstract protected function action(): ReconcileRepositories;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Base;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Storage\StorageResults;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class UploadResults.
|
||||
*/
|
||||
class UploadResults extends StorageResults
|
||||
{
|
||||
/**
|
||||
* Create a new action result instance.
|
||||
*
|
||||
* @param array<string, string|false> $uploads
|
||||
*/
|
||||
public function __construct(protected readonly array $uploads = [])
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Write results to log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
if (empty($this->uploads)) {
|
||||
Log::error('No uploads were attempted.');
|
||||
}
|
||||
foreach ($this->uploads as $fs => $result) {
|
||||
$result === false
|
||||
? Log::error("Failed to upload to disk '$fs'")
|
||||
: Log::info("Uploaded '$result' to disk '$fs'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to Action Result.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function toActionResult(): ActionResult
|
||||
{
|
||||
if (empty($this->uploads)) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
'No uploads were attempted. Please check that disks are configured.'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var Collection $failed */
|
||||
/** @var Collection $passed */
|
||||
[$failed, $passed] = collect($this->uploads)->partition(fn (string|false $result, string $fs) => $result === false);
|
||||
|
||||
if ($failed->isNotEmpty()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"Failed to upload to disks {$failed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED(),
|
||||
"Uploaded '{$passed->values()->first()}' to disks {$passed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage;
|
||||
|
||||
/**
|
||||
* Class StorageAction.
|
||||
*/
|
||||
abstract class StorageAction
|
||||
{
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return StorageResults
|
||||
*/
|
||||
abstract public function handle(): StorageResults;
|
||||
|
||||
/**
|
||||
* Get the disks to update.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function disks(): array;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
|
||||
/**
|
||||
* Class StorageResults.
|
||||
*/
|
||||
abstract class StorageResults
|
||||
{
|
||||
/**
|
||||
* Write results to log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function toLog(): void;
|
||||
|
||||
/**
|
||||
* Transform to Action Result.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
abstract public function toActionResult(): ActionResult;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Audio;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositories;
|
||||
use App\Actions\Storage\Base\DeleteAction;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Models\Wiki\Audio;
|
||||
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
|
||||
use App\Repositories\Storage\Wiki\AudioRepository as AudioSourceRepository;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* Class DeleteAudioAction.
|
||||
*
|
||||
* @extends DeleteAction<Audio>
|
||||
*/
|
||||
class DeleteAudioAction extends DeleteAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Audio $audio
|
||||
*/
|
||||
public function __construct(Audio $audio)
|
||||
{
|
||||
parent::__construct($audio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disks to update.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function disks(): array
|
||||
{
|
||||
return Config::get(AudioConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
protected function getSourceRepository(): RepositoryInterface
|
||||
{
|
||||
return App::make(AudioSourceRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
protected function getDestinationRepository(): RepositoryInterface
|
||||
{
|
||||
return App::make(AudioDestinationRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reconcile action.
|
||||
*
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
protected function action(): ReconcileRepositories
|
||||
{
|
||||
return new ReconcileAudioRepositories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to delete.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function path(): string
|
||||
{
|
||||
return $this->model->path();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Audio;
|
||||
|
||||
use App\Actions\Storage\Base\MoveAction;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
/**
|
||||
* Class MoveAudioAction.
|
||||
*
|
||||
* @extends MoveAction<Audio>
|
||||
*/
|
||||
class MoveAudioAction extends MoveAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Audio $audio
|
||||
* @param string $to
|
||||
*/
|
||||
public function __construct(Audio $audio, string $to)
|
||||
{
|
||||
parent::__construct($audio, $to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disks to update.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function disks(): array
|
||||
{
|
||||
return Config::get(AudioConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to move from.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function from(): string
|
||||
{
|
||||
return $this->model->path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update underlying model.
|
||||
* We want to apply these updates through Eloquent to preserve relations when renaming.
|
||||
* Otherwise, reconciliation would destroy the old model and create a new model for the new name.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function update(): void
|
||||
{
|
||||
$this->model->update([
|
||||
Audio::ATTRIBUTE_BASENAME => File::basename($this->to),
|
||||
Audio::ATTRIBUTE_FILENAME => File::name($this->to),
|
||||
Audio::ATTRIBUTE_PATH => $this->to,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Audio;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositories;
|
||||
use App\Actions\Storage\Base\UploadAction;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
|
||||
use App\Repositories\Storage\Wiki\AudioRepository as AudioSourceRepository;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* Class UploadAudioAction.
|
||||
*/
|
||||
class UploadAudioAction extends UploadAction
|
||||
{
|
||||
/**
|
||||
* Get the disks to update.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function disks(): array
|
||||
{
|
||||
return Config::get(AudioConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
protected function getSourceRepository(): RepositoryInterface
|
||||
{
|
||||
return App::make(AudioSourceRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
protected function getDestinationRepository(): RepositoryInterface
|
||||
{
|
||||
return App::make(AudioDestinationRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reconcile action.
|
||||
*
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
protected function action(): ReconcileRepositories
|
||||
{
|
||||
return new ReconcileAudioRepositories();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Video;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\Wiki\Video\ReconcileVideoRepositories;
|
||||
use App\Actions\Storage\Base\DeleteAction;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Models\Wiki\Video;
|
||||
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
|
||||
use App\Repositories\Storage\Wiki\VideoRepository as VideoSourceRepository;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* Class DeleteVideoAction.
|
||||
*
|
||||
* @extends DeleteAction<Video>
|
||||
*/
|
||||
class DeleteVideoAction extends DeleteAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Video $video
|
||||
*/
|
||||
public function __construct(Video $video)
|
||||
{
|
||||
parent::__construct($video);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disks to update.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function disks(): array
|
||||
{
|
||||
return Config::get(VideoConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
protected function getSourceRepository(): RepositoryInterface
|
||||
{
|
||||
return App::make(VideoSourceRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
protected function getDestinationRepository(): RepositoryInterface
|
||||
{
|
||||
return App::make(VideoDestinationRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reconcile action.
|
||||
*
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
protected function action(): ReconcileRepositories
|
||||
{
|
||||
return new ReconcileVideoRepositories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to delete.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function path(): string
|
||||
{
|
||||
return $this->model->path();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Video;
|
||||
|
||||
use App\Actions\Storage\Base\MoveAction;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
/**
|
||||
* Class MoveVideoAction.
|
||||
*
|
||||
* @extends MoveAction<Video>
|
||||
*/
|
||||
class MoveVideoAction extends MoveAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Video $video
|
||||
* @param string $to
|
||||
*/
|
||||
public function __construct(Video $video, string $to)
|
||||
{
|
||||
parent::__construct($video, $to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disks to update.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function disks(): array
|
||||
{
|
||||
return Config::get(VideoConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to move from.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function from(): string
|
||||
{
|
||||
return $this->model->path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update underlying model.
|
||||
* We want to apply these updates through Eloquent to preserve relations when renaming.
|
||||
* Otherwise, reconciliation would destroy the old model and create a new model for the new name.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function update(): void
|
||||
{
|
||||
$this->model->update([
|
||||
Video::ATTRIBUTE_BASENAME => File::basename($this->to),
|
||||
Video::ATTRIBUTE_FILENAME => File::name($this->to),
|
||||
Video::ATTRIBUTE_PATH => $this->to,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Video;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\Wiki\Video\ReconcileVideoRepositories;
|
||||
use App\Actions\Storage\Base\UploadAction;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
|
||||
use App\Repositories\Storage\Wiki\VideoRepository as VideoSourceRepository;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* Class UploadVideoAction.
|
||||
*/
|
||||
class UploadVideoAction extends UploadAction
|
||||
{
|
||||
/**
|
||||
* Get the disks to update.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function disks(): array
|
||||
{
|
||||
return Config::get(VideoConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
protected function getSourceRepository(): RepositoryInterface
|
||||
{
|
||||
return App::make(VideoSourceRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @return RepositoryInterface
|
||||
*/
|
||||
protected function getDestinationRepository(): RepositoryInterface
|
||||
{
|
||||
return App::make(VideoDestinationRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reconcile action.
|
||||
*
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
protected function action(): ReconcileRepositories
|
||||
{
|
||||
return new ReconcileVideoRepositories();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns\Repositories\Billing;
|
||||
|
||||
use App\Concerns\Repositories\ReconcilesRepositories;
|
||||
use App\Enums\Http\Api\Filter\AllowedDateFormat;
|
||||
use App\Models\Billing\Transaction;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Trait ReconcilesTransactionRepositories.
|
||||
*/
|
||||
trait ReconcilesTransactionRepositories
|
||||
{
|
||||
use ReconcilesRepositories;
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Transaction::ATTRIBUTE_AMOUNT,
|
||||
Transaction::ATTRIBUTE_DATE,
|
||||
Transaction::ATTRIBUTE_EXTERNAL_ID,
|
||||
Transaction::ATTRIBUTE_ID,
|
||||
Transaction::ATTRIBUTE_SERVICE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Transaction $first, Transaction $second) => [$first->external_id, $first->date->format(AllowedDateFormat::YMD), $first->amount]
|
||||
<=> [$second->external_id, $second->date->format(AllowedDateFormat::YMD), $second->amount];
|
||||
}
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns\Repositories;
|
||||
|
||||
use App\Contracts\Repositories\Repository;
|
||||
use App\Models\BaseModel;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Trait ReconcilesRepositories.
|
||||
*/
|
||||
trait ReconcilesRepositories
|
||||
{
|
||||
/**
|
||||
* The number of models created.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $created = 0;
|
||||
|
||||
/**
|
||||
* The number of models whose creation failed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $createdFailed = 0;
|
||||
|
||||
/**
|
||||
* The number of models deleted.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $deleted = 0;
|
||||
|
||||
/**
|
||||
* The number of models whose deletion failed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $deletedFailed = 0;
|
||||
|
||||
/**
|
||||
* The number of models updated.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $updated = 0;
|
||||
|
||||
/**
|
||||
* The number of models whose update failed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $updatedFailed = 0;
|
||||
|
||||
/**
|
||||
* Callback for successful model creation.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCreated(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for failed model creation.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedCreation(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for successful model deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleDeleted(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for failed model deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedDeletion(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for successful model update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleUpdated(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for failed model update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedUpdate(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for exception.
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*/
|
||||
protected function handleException(Exception $exception): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for handling completion of reconciliation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function postReconciliationTask(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any changes, successful or not, were made during reconciliation.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasResults(): bool
|
||||
{
|
||||
return $this->hasChanges() || $this->hasFailures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any successful changes were made during reconciliation.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasChanges(): bool
|
||||
{
|
||||
return $this->created > 0 || $this->deleted > 0 || $this->updated > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any unsuccessful changes were made during reconciliation.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasFailures(): bool
|
||||
{
|
||||
return $this->createdFailed > 0 || $this->deletedFailed > 0 || $this->updatedFailed > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform set reconciliation between source and destination repositories.
|
||||
*
|
||||
* @param Repository $source
|
||||
* @param Repository $destination
|
||||
* @return void
|
||||
*/
|
||||
public function reconcileRepositories(Repository $source, Repository $destination): void
|
||||
{
|
||||
try {
|
||||
$sourceModels = $source->get();
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForCreateDelete());
|
||||
|
||||
$this->createModelsFromSource($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$this->deleteModelsFromDestination($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForUpdate());
|
||||
|
||||
$this->updateDestinationModels($destination, $sourceModels, $destinationModels);
|
||||
} catch (Exception $exception) {
|
||||
$this->handleException($exception);
|
||||
} finally {
|
||||
$this->postReconciliationTask();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn () => 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create models that exist in source but not in destination.
|
||||
*
|
||||
* @param Repository $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return void
|
||||
*/
|
||||
protected function createModelsFromSource(
|
||||
Repository $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): void {
|
||||
$createModels = $sourceModels->diffUsing($destinationModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
foreach ($createModels as $createModel) {
|
||||
$createResult = $destination->save($createModel);
|
||||
if ($createResult) {
|
||||
$this->created++;
|
||||
$this->handleCreated($createModel);
|
||||
} else {
|
||||
$this->createdFailed++;
|
||||
$this->handleFailedCreation($createModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete models that exist in destination but not in source.
|
||||
*
|
||||
* @param Repository $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return void
|
||||
*/
|
||||
protected function deleteModelsFromDestination(
|
||||
Repository $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): void {
|
||||
$deleteModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
foreach ($deleteModels as $deleteModel) {
|
||||
$deleteResult = $destination->delete($deleteModel);
|
||||
if ($deleteResult) {
|
||||
$this->deleted++;
|
||||
$this->handleDeleted($deleteModel);
|
||||
} else {
|
||||
$this->deletedFailed++;
|
||||
$this->handleFailedDeletion($deleteModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn () => 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*
|
||||
* @param Collection $sourceModels
|
||||
* @param Model $destinationModel
|
||||
* @return Model|null
|
||||
*/
|
||||
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update destination models that have changed in source.
|
||||
*
|
||||
* @param Repository $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return void
|
||||
*/
|
||||
protected function updateDestinationModels(
|
||||
Repository $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): void {
|
||||
$updatedModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForUpdate());
|
||||
|
||||
foreach ($updatedModels as $updatedModel) {
|
||||
$sourceModel = $this->resolveUpdatedModel($sourceModels, $updatedModel);
|
||||
if ($sourceModel !== null) {
|
||||
$updateResult = $destination->update($updatedModel, $sourceModel->toArray());
|
||||
if ($updateResult) {
|
||||
$this->updated++;
|
||||
$this->handleUpdated($updatedModel);
|
||||
} else {
|
||||
$this->updatedFailed++;
|
||||
$this->handleFailedUpdate($updatedModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Billing\Balance;
|
||||
|
||||
use App\Concerns\Repositories\Billing\ReconcilesBalanceRepositories;
|
||||
use App\Actions\Repositories\Billing\Balance\ReconcileBalanceRepositories;
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Console\Commands\Billing\ServiceReconcileCommand;
|
||||
use App\Contracts\Repositories\Repository;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Enums\Models\Billing\Service;
|
||||
use App\Models\BaseModel;
|
||||
use App\Repositories\DigitalOcean\Billing\DigitalOceanBalanceRepository as DigitalOceanSourceRepository;
|
||||
use App\Repositories\Eloquent\Billing\DigitalOceanBalanceRepository as DigitalOceanDestinationRepository;
|
||||
use App\Repositories\Service\DigitalOcean\Billing\DigitalOceanBalanceRepository as DigitalOceanSourceRepository;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BalanceReconcileCommand.
|
||||
*/
|
||||
class BalanceReconcileCommand extends ServiceReconcileCommand
|
||||
{
|
||||
use ReconcilesBalanceRepositories;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
@@ -37,121 +34,25 @@ class BalanceReconcileCommand extends ServiceReconcileCommand
|
||||
protected $description = 'Perform set reconciliation between vendor billing API and balance database';
|
||||
|
||||
/**
|
||||
* Print the result to console and log the results to the app log.
|
||||
* Get the reconciliation action.
|
||||
*
|
||||
* @return void
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
protected function postReconciliationTask(): void
|
||||
protected function getAction(): ReconcileRepositories
|
||||
{
|
||||
if ($this->hasResults()) {
|
||||
if ($this->hasChanges()) {
|
||||
Log::info("$this->created Balances created, $this->deleted Balances deleted, $this->updated Balances updated");
|
||||
$this->info("$this->created Balances created, $this->deleted Balances deleted, $this->updated Balances updated");
|
||||
}
|
||||
if ($this->hasFailures()) {
|
||||
Log::error("Failed to create $this->createdFailed Balances, delete $this->deletedFailed Balances, update $this->updatedFailed Balances");
|
||||
$this->error("Failed to create $this->createdFailed Balances, delete $this->deletedFailed Balances, update $this->updatedFailed Balances");
|
||||
}
|
||||
} else {
|
||||
Log::info('No Balances created or deleted or updated');
|
||||
$this->info('No Balances created or deleted or updated');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful balance creation.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCreated(BaseModel $model): void
|
||||
{
|
||||
Log::info("Balance '{$model->getName()}' created");
|
||||
$this->info("Balance '{$model->getName()}' created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed balance creation.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedCreation(BaseModel $model): void
|
||||
{
|
||||
Log::error("Balance '{$model->getName()}' was not created");
|
||||
$this->error("Balance '{$model->getName()}' was not created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful balance deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleDeleted(BaseModel $model): void
|
||||
{
|
||||
Log::info("Balance '{$model->getName()}' deleted");
|
||||
$this->info("Balance '{$model->getName()}' deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed balance deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedDeletion(BaseModel $model): void
|
||||
{
|
||||
Log::error("Balance '{$model->getName()}' was not deleted");
|
||||
$this->error("Balance '{$model->getName()}' was not deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful balance update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleUpdated(BaseModel $model): void
|
||||
{
|
||||
Log::info("Balance '{$model->getName()}' updated");
|
||||
$this->info("Balance '{$model->getName()}' updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed balance update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedUpdate(BaseModel $model): void
|
||||
{
|
||||
Log::error("Balance '{$model->getName()}' was not updated");
|
||||
$this->error("Balance '{$model->getName()}' was not updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for exception.
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*/
|
||||
protected function handleException(Exception $exception): void
|
||||
{
|
||||
Log::error($exception->getMessage());
|
||||
$this->error($exception->getMessage());
|
||||
return new ReconcileBalanceRepositories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function getSourceRepository(Service $service): ?Repository
|
||||
protected function getSourceRepository(array $validated): ?RepositoryInterface
|
||||
{
|
||||
return match ($service->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
|
||||
return match (Arr::get($validated, 'service')) {
|
||||
Service::DIGITALOCEAN()->key => App::make(DigitalOceanSourceRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
@@ -159,13 +60,13 @@ class BalanceReconcileCommand extends ServiceReconcileCommand
|
||||
/**
|
||||
* Get destination repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function getDestinationRepository(Service $service): ?Repository
|
||||
protected function getDestinationRepository(array $validated): ?RepositoryInterface
|
||||
{
|
||||
return match ($service->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanDestinationRepository::class),
|
||||
return match (Arr::get($validated, 'service')) {
|
||||
Service::DIGITALOCEAN()->key => App::make(DigitalOceanDestinationRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,77 +4,42 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Billing;
|
||||
|
||||
use App\Contracts\Repositories\Repository;
|
||||
use App\Console\Commands\ReconcileCommand;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Enums\Models\Billing\Service;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use BenSampo\Enum\Rules\EnumKey;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
|
||||
/**
|
||||
* Class ServiceReconcileCommand.
|
||||
*/
|
||||
abstract class ServiceReconcileCommand extends Command
|
||||
abstract class ServiceReconcileCommand extends ReconcileCommand
|
||||
{
|
||||
/**
|
||||
* Execute the console command.
|
||||
* Get the validator for options.
|
||||
*
|
||||
* @return int
|
||||
* @return Validator
|
||||
*/
|
||||
public function handle(): int
|
||||
protected function validator(): Validator
|
||||
{
|
||||
$key = $this->argument('service');
|
||||
$service = Service::coerce(Str::upper($key));
|
||||
|
||||
if ($service === null) {
|
||||
Log::error("Invalid Service '$key'");
|
||||
$this->error("Invalid Service '$key'");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$sourceRepository = $this->getSourceRepository($service);
|
||||
if ($sourceRepository === null) {
|
||||
Log::error("No source repository implemented for Service '$key'");
|
||||
$this->error("No source repository implemented for Service '$key'");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$destinationRepository = $this->getDestinationRepository($service);
|
||||
if ($destinationRepository === null) {
|
||||
Log::error("No destination repository implemented for Service '$key'");
|
||||
$this->error("No destination repository implemented for Service '$key'");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->reconcileRepositories($sourceRepository, $destinationRepository);
|
||||
|
||||
return 0;
|
||||
return \Illuminate\Support\Facades\Validator::make($this->arguments(), [
|
||||
'service' => ['required', new EnumKey(Service::class)],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform set reconciliation between source and destination repositories.
|
||||
* Apply filters to repositories before reconciliation.
|
||||
*
|
||||
* @param Repository $source
|
||||
* @param Repository $destination
|
||||
* @param array $validated
|
||||
* @param RepositoryInterface $sourceRepository
|
||||
* @param RepositoryInterface $destinationRepository
|
||||
* @return void
|
||||
*/
|
||||
abstract public function reconcileRepositories(Repository $source, Repository $destination): void;
|
||||
|
||||
/**
|
||||
* Get source repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
*/
|
||||
abstract protected function getSourceRepository(Service $service): ?Repository;
|
||||
|
||||
/**
|
||||
* Get destination repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
*/
|
||||
abstract protected function getDestinationRepository(Service $service): ?Repository;
|
||||
protected function handleFilters(
|
||||
array $validated,
|
||||
RepositoryInterface $sourceRepository,
|
||||
RepositoryInterface $destinationRepository
|
||||
): void {
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Billing\Transaction;
|
||||
|
||||
use App\Concerns\Repositories\Billing\ReconcilesTransactionRepositories;
|
||||
use App\Actions\Repositories\Billing\Transaction\ReconcileTransactionRepositories;
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Console\Commands\Billing\ServiceReconcileCommand;
|
||||
use App\Contracts\Repositories\Repository;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Enums\Models\Billing\Service;
|
||||
use App\Models\Billing\Transaction;
|
||||
use App\Repositories\DigitalOcean\Billing\DigitalOceanTransactionRepository as DigitalOceanSourceRepository;
|
||||
use App\Repositories\Eloquent\Billing\DigitalOceanTransactionRepository as DigitalOceanDestinationRepository;
|
||||
use App\Repositories\Service\DigitalOcean\Billing\DigitalOceanTransactionRepository as DigitalOceanSourceRepository;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class TransactionReconcileCommand.
|
||||
*/
|
||||
class TransactionReconcileCommand extends ServiceReconcileCommand
|
||||
{
|
||||
use ReconcilesTransactionRepositories;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
@@ -37,121 +34,25 @@ class TransactionReconcileCommand extends ServiceReconcileCommand
|
||||
protected $description = 'Perform set reconciliation between vendor billing API and transaction database';
|
||||
|
||||
/**
|
||||
* Print the result to console and log the results to the app log.
|
||||
* Get the reconciliation action.
|
||||
*
|
||||
* @return void
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
protected function postReconciliationTask(): void
|
||||
protected function getAction(): ReconcileRepositories
|
||||
{
|
||||
if ($this->hasResults()) {
|
||||
if ($this->hasChanges()) {
|
||||
Log::info("$this->created Transactions created, $this->deleted Transactions deleted, $this->updated Transactions updated");
|
||||
$this->info("$this->created Transactions created, $this->deleted Transactions deleted, $this->updated Transactions updated");
|
||||
}
|
||||
if ($this->hasFailures()) {
|
||||
Log::error("Failed to create $this->createdFailed Transactions, delete $this->deletedFailed Transactions, update $this->updatedFailed Transactions");
|
||||
$this->error("Failed to create $this->createdFailed Transactions, delete $this->deletedFailed Transactions, update $this->updatedFailed Transactions");
|
||||
}
|
||||
} else {
|
||||
Log::info('No Transactions created or deleted or updated');
|
||||
$this->info('No Transactions created or deleted or updated');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful transaction creation.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCreated(Transaction $model): void
|
||||
{
|
||||
Log::info("Transaction '{$model->getName()}' created");
|
||||
$this->info("Transaction '{$model->getName()}' created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed transaction creation.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedCreation(Transaction $model): void
|
||||
{
|
||||
Log::error("Transaction '{$model->getName()}' was not created");
|
||||
$this->error("Transaction '{$model->getName()}' was not created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful transaction deletion.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleDeleted(Transaction $model): void
|
||||
{
|
||||
Log::info("Transaction '{$model->getName()}' deleted");
|
||||
$this->info("Transaction '{$model->getName()}' deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed transaction deletion.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedDeletion(Transaction $model): void
|
||||
{
|
||||
Log::error("Transaction '{$model->getName()}' was not deleted");
|
||||
$this->error("Transaction '{$model->getName()}' was not deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful transaction update.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleUpdated(Transaction $model): void
|
||||
{
|
||||
Log::info("Transaction '{$model->getName()}' updated");
|
||||
$this->info("Transaction '{$model->getName()}' updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed transaction update.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedUpdate(Transaction $model): void
|
||||
{
|
||||
Log::error("Transaction '{$model->getName()}' was not updated");
|
||||
$this->error("Transaction '{$model->getName()}' was not updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for exception.
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*/
|
||||
protected function handleException(Exception $exception): void
|
||||
{
|
||||
Log::error($exception->getMessage());
|
||||
$this->error($exception->getMessage());
|
||||
return new ReconcileTransactionRepositories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function getSourceRepository(Service $service): ?Repository
|
||||
protected function getSourceRepository(array $validated): ?RepositoryInterface
|
||||
{
|
||||
return match ($service->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
|
||||
return match (Arr::get($validated, 'service')) {
|
||||
Service::DIGITALOCEAN()->key => App::make(DigitalOceanSourceRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
@@ -159,13 +60,13 @@ class TransactionReconcileCommand extends ServiceReconcileCommand
|
||||
/**
|
||||
* Get destination repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function getDestinationRepository(Service $service): ?Repository
|
||||
protected function getDestinationRepository(array $validated): ?RepositoryInterface
|
||||
{
|
||||
return match ($service->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanDestinationRepository::class),
|
||||
return match (Arr::get($validated, 'service')) {
|
||||
Service::DIGITALOCEAN()->key => App::make(DigitalOceanDestinationRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Console\Commands;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\MySqlConnection;
|
||||
use Illuminate\Database\PostgresConnection;
|
||||
use Illuminate\Database\SQLiteConnection;
|
||||
@@ -14,12 +14,16 @@ use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
use Spatie\DbDumper\Databases\MySql;
|
||||
use Spatie\DbDumper\Databases\PostgreSql;
|
||||
use Spatie\DbDumper\Databases\Sqlite;
|
||||
use Spatie\DbDumper\DbDumper;
|
||||
use Spatie\DbDumper\Exceptions\CannotSetParameter;
|
||||
|
||||
/**
|
||||
* Class DatabaseDumpCommand.
|
||||
@@ -33,12 +37,24 @@ abstract class DatabaseDumpCommand extends Command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$create = $this->option('create');
|
||||
$validator = Validator::make($this->options(), [
|
||||
'default-character-set' => ['string'],
|
||||
'set-gtid-purged' => [Rule::in(['OFF', 'ON', 'AUTO'])->__toString()],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
foreach ($validator->errors()->all() as $error) {
|
||||
$this->error($error);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var Connection $connection */
|
||||
$connection = DB::connection();
|
||||
|
||||
$dumper = $this->getDumper($connection, $create);
|
||||
$dumper = $this->getDumper($connection);
|
||||
if ($dumper === null) {
|
||||
Log::error("Unrecognized connection '{$connection->getName()}'");
|
||||
$this->error("Unrecognized connection '{$connection->getName()}'");
|
||||
@@ -46,29 +62,7 @@ abstract class DatabaseDumpCommand extends Command
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (! static::canIncludeTables($connection)) {
|
||||
Log::error('DB connection does not support includeTables option');
|
||||
$this->error('DB connection does not support includeTables option');
|
||||
|
||||
return 1;
|
||||
}
|
||||
$dumper->includeTables($this->allowedTables());
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName())
|
||||
->setUserName(strval($connection->getConfig('username')))
|
||||
->setPassword(strval($connection->getConfig('password')));
|
||||
|
||||
$host = $connection->getConfig('host');
|
||||
if ($host !== null) {
|
||||
$dumper->setHost(collect($host)->first());
|
||||
}
|
||||
|
||||
$port = $connection->getConfig('port');
|
||||
if (is_int($port)) {
|
||||
$dumper->setPort($port);
|
||||
}
|
||||
|
||||
$dumpFile = $this->getDumpFile($create);
|
||||
$dumpFile = $this->getDumpFile();
|
||||
|
||||
$dumper->dumpToFile($dumpFile);
|
||||
|
||||
@@ -89,44 +83,173 @@ abstract class DatabaseDumpCommand extends Command
|
||||
/**
|
||||
* Get the dumper for the database connection.
|
||||
*
|
||||
* @param ConnectionInterface $connection
|
||||
* @param bool $create
|
||||
* @param Connection $connection
|
||||
* @return DbDumper|null
|
||||
*
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function getDumper(ConnectionInterface $connection, bool $create): ?DbDumper
|
||||
protected function getDumper(Connection $connection): ?DbDumper
|
||||
{
|
||||
return match (get_class($connection)) {
|
||||
SQLiteConnection::class => Sqlite::create(),
|
||||
MySqlConnection::class => $create ? MySql::create() : MySql::create()->doNotCreateTables(),
|
||||
PostgresConnection::class => $create ? PostgreSql::create() : PostgreSql::create()->doNotCreateTables(),
|
||||
SQLiteConnection::class => $this->prepareSqliteDumper($connection),
|
||||
MySqlConnection::class => $this->prepareMySqlDumper($connection),
|
||||
PostgresConnection::class => $this->preparePostgreSqlDumper($connection),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the database connection supports table inclusion.
|
||||
* Configure Sqlite database dumper.
|
||||
*
|
||||
* @param ConnectionInterface $connection
|
||||
* @return bool
|
||||
* @param Connection $connection
|
||||
* @return Sqlite
|
||||
*
|
||||
* @throws RuntimeException
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
public static function canIncludeTables(ConnectionInterface $connection): bool
|
||||
protected function prepareSqliteDumper(Connection $connection): Sqlite
|
||||
{
|
||||
// Sqlite version 3.32.0 is required when using the includeTables option
|
||||
if ($connection instanceof SQLiteConnection) {
|
||||
return version_compare($connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), '3.32.0', '>=');
|
||||
if (version_compare($connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), '3.32.0', '<')) {
|
||||
throw new RuntimeException('DB connection does not support includeTables option');
|
||||
}
|
||||
|
||||
return true;
|
||||
$dumper = Sqlite::create();
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName());
|
||||
$dumper->includeTables($this->allowedTables());
|
||||
|
||||
return $dumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure MySQL database dumper.
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @return MySql
|
||||
*
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function prepareMySqlDumper(Connection $connection): MySql
|
||||
{
|
||||
$dumper = MySql::create();
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName());
|
||||
$dumper->setUserName(strval($connection->getConfig('username')));
|
||||
$dumper->setPassword(strval($connection->getConfig('password')));
|
||||
|
||||
$hostConfig = $connection->getConfig('host');
|
||||
if ($hostConfig !== null) {
|
||||
$dumper->setHost(collect($hostConfig)->first());
|
||||
}
|
||||
|
||||
$port = $connection->getConfig('port');
|
||||
if (is_numeric($port)) {
|
||||
$dumper->setPort(intval($port));
|
||||
}
|
||||
|
||||
$dumper->includeTables($this->allowedTables());
|
||||
|
||||
if ($this->option('comments')) {
|
||||
$dumper->dontSkipComments();
|
||||
}
|
||||
|
||||
if ($this->option('skip-comments')) {
|
||||
$dumper->skipComments();
|
||||
}
|
||||
|
||||
if ($this->option('extended-insert')) {
|
||||
$dumper->useExtendedInserts();
|
||||
}
|
||||
|
||||
if ($this->option('skip-extended-insert')) {
|
||||
$dumper->dontUseExtendedInserts();
|
||||
}
|
||||
|
||||
$this->option('single-transaction')
|
||||
? $dumper->useSingleTransaction()
|
||||
: $dumper->dontUseSingleTransaction();
|
||||
|
||||
if ($this->option('lock-tables')) {
|
||||
$dumper->dontSkipLockTables();
|
||||
}
|
||||
|
||||
if ($this->option('skip-lock-tables')) {
|
||||
$dumper->skipLockTables();
|
||||
}
|
||||
|
||||
if ($this->option('skip-column-statistics')) {
|
||||
$dumper->doNotUseColumnStatistics();
|
||||
}
|
||||
|
||||
if ($this->option('quick')) {
|
||||
$dumper->useQuick();
|
||||
}
|
||||
|
||||
if ($this->option('skip-quick')) {
|
||||
$dumper->dontUseQuick();
|
||||
}
|
||||
|
||||
if ($this->hasOption('default-character-set')) {
|
||||
$dumper->setDefaultCharacterSet($this->option('default-character-set'));
|
||||
}
|
||||
|
||||
if ($this->hasOption('set-gtid-purged')) {
|
||||
$dumper->setGtidPurged($this->option('set-gtid-purged'));
|
||||
}
|
||||
|
||||
if ($this->option('no-create-info')) {
|
||||
$dumper->doNotCreateTables();
|
||||
}
|
||||
|
||||
return $dumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure PostgreSql database dumper.
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @return PostgreSql
|
||||
*
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function preparePostgreSqlDumper(Connection $connection): PostgreSql
|
||||
{
|
||||
$dumper = PostgreSql::create();
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName());
|
||||
$dumper->setUserName(strval($connection->getConfig('username')));
|
||||
$dumper->setPassword(strval($connection->getConfig('password')));
|
||||
|
||||
$host = $connection->getConfig('host');
|
||||
if ($host !== null) {
|
||||
$dumper->setHost(collect($host)->first());
|
||||
}
|
||||
|
||||
$port = $connection->getConfig('port');
|
||||
if (is_int($port)) {
|
||||
$dumper->setPort($port);
|
||||
}
|
||||
|
||||
$dumper->includeTables($this->allowedTables());
|
||||
|
||||
if ($this->option('inserts')) {
|
||||
$dumper->useInserts();
|
||||
}
|
||||
|
||||
if ($this->option('data-only')) {
|
||||
$dumper->doNotCreateTables();
|
||||
}
|
||||
|
||||
return $dumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The target path for the database dump.
|
||||
* Pattern: "/path/to/project/storage/db-dumps/{path}/animethemes-db-dump-{create?}-{year}-{month}-{day}.sql".
|
||||
* Pattern: "/path/to/project/storage/db-dumps/{path}/animethemes-db-dump-{milliseconds from epoch}.sql".
|
||||
*
|
||||
* @param bool $create
|
||||
* @return string
|
||||
*/
|
||||
public function getDumpFile(bool $create = false): string
|
||||
protected function getDumpFile(): string
|
||||
{
|
||||
$filesystem = Storage::disk('db-dumps');
|
||||
|
||||
@@ -135,8 +258,7 @@ abstract class DatabaseDumpCommand extends Command
|
||||
return Str::of($filesystem->path($this->getDumpFilePath()))
|
||||
->append(DIRECTORY_SEPARATOR)
|
||||
->append('animethemes-db-dump-')
|
||||
->append($create ? 'create-' : '')
|
||||
->append(Date::now()->toDateString())
|
||||
->append(intval(Date::now()->valueOf()))
|
||||
->append('.sql')
|
||||
->__toString();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,23 @@ class DocumentDatabaseDumpCommand extends DatabaseDumpCommand
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'db:dump-document {--C|create : Whether the dumper should include create table statements}';
|
||||
protected $signature = 'db:dump-document
|
||||
{--comments : Write additional information in the MySQL dump such as program version, server version and host}
|
||||
{--data-only : Dump only the data without the schema in PostgreSQL dump}
|
||||
{--default-character-set=utf8 : Specify default character set in MySQL dump}
|
||||
{--extended-insert : Use multiple-row insert syntax in MySQL dump}
|
||||
{--inserts : Dump data as INSERT commands rather than COPY in PostgreSQL dump}
|
||||
{--lock-tables : Lock all tables before dumping them to MySQL dump}
|
||||
{--no-create-info : Turn off CREATE TABLE statements in MySQL dump}
|
||||
{--quick : Retrieve rows for a table from the server one row at a time in MySQL dump}
|
||||
{--set-gtid-purged=AUTO : Add SET GTID_PURGED to output in MySQL dump}
|
||||
{--single-transaction : Issue a BEGIN SQL statement before dumping data from server for MySQL dump}
|
||||
{--skip-column-statistics : Turn off ANALYZE table statements in the MySQL dump}
|
||||
{--skip-comments : Do not write additional information in the MySQL dump}
|
||||
{--skip-extended-insert : Turn off extended-insert in MySQL dump}
|
||||
{--skip-lock-tables : Turn off locking tables before dumping to MySQL dump}
|
||||
{--skip-quick : Do not retrieve rows for a table from the server one row at a time in MySQL dump}
|
||||
';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* Class ReconcileCommand.
|
||||
*/
|
||||
abstract class ReconcileCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$validator = $this->validator();
|
||||
if ($validator->fails()) {
|
||||
foreach ($validator->errors()->all() as $error) {
|
||||
$this->error($error);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$sourceRepository = $this->getSourceRepository($validator->validated());
|
||||
if ($sourceRepository === null) {
|
||||
Log::error('Could not find source repository', $validator->validated());
|
||||
$this->error('Could not find source repository');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$destinationRepository = $this->getDestinationRepository($validator->validated());
|
||||
if ($destinationRepository === null) {
|
||||
Log::error('Could not find destination repository', $validator->validated());
|
||||
$this->error('Could not find destination repository');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->handleFilters($validator->validated(), $sourceRepository, $destinationRepository);
|
||||
|
||||
$action = $this->getAction();
|
||||
|
||||
$results = $action->reconcileRepositories($sourceRepository, $destinationRepository);
|
||||
|
||||
$results->toLog();
|
||||
$results->toConsole($this);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validator for options.
|
||||
*
|
||||
* @return Validator
|
||||
*/
|
||||
abstract protected function validator(): Validator;
|
||||
|
||||
/**
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
abstract protected function getSourceRepository(array $validated): ?RepositoryInterface;
|
||||
|
||||
/**
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
abstract protected function getDestinationRepository(array $validated): ?RepositoryInterface;
|
||||
|
||||
/**
|
||||
* Apply filters to repositories before reconciliation.
|
||||
*
|
||||
* @param array $validated
|
||||
* @param RepositoryInterface $sourceRepository
|
||||
* @param RepositoryInterface $destinationRepository
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function handleFilters(
|
||||
array $validated,
|
||||
RepositoryInterface $sourceRepository,
|
||||
RepositoryInterface $destinationRepository
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Get the reconciliation action.
|
||||
*
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
abstract protected function getAction(): ReconcileRepositories;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Wiki\Audio;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositories;
|
||||
use App\Console\Commands\Wiki\StorageReconcileCommand;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
|
||||
use App\Repositories\Storage\Wiki\AudioRepository as AudioSourceRepository;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* Class AudioReconcileCommand.
|
||||
*/
|
||||
class AudioReconcileCommand extends StorageReconcileCommand
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'reconcile:audio
|
||||
{--path= : The directory of audios to reconcile. Ex: 2022/Spring/. If unspecified, all directories will be listed.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Perform set reconciliation between object storage and audios database';
|
||||
|
||||
/**
|
||||
* Get the name of the disk that represents the filesystem.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function disk(): string
|
||||
{
|
||||
return Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function getSourceRepository(array $validated): ?RepositoryInterface
|
||||
{
|
||||
return App::make(AudioSourceRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function getDestinationRepository(array $validated): ?RepositoryInterface
|
||||
{
|
||||
return App::make(AudioDestinationRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reconciliation action.
|
||||
*
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
protected function getAction(): ReconcileRepositories
|
||||
{
|
||||
return new ReconcileAudioRepositories();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Wiki;
|
||||
|
||||
use App\Console\Commands\ReconcileCommand;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Rules\Storage\StorageDirectoryExistsRule;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Class StorageReconcileCommand.
|
||||
*/
|
||||
abstract class StorageReconcileCommand extends ReconcileCommand
|
||||
{
|
||||
/**
|
||||
* Get the name of the disk that represents the filesystem.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function disk(): string;
|
||||
|
||||
/**
|
||||
* Apply filters to repositories before reconciliation.
|
||||
*
|
||||
* @param array $validated
|
||||
* @param RepositoryInterface $sourceRepository
|
||||
* @param RepositoryInterface $destinationRepository
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFilters(
|
||||
array $validated,
|
||||
RepositoryInterface $sourceRepository,
|
||||
RepositoryInterface $destinationRepository
|
||||
): void {
|
||||
$path = Arr::get($validated, 'path');
|
||||
if ($path !== null) {
|
||||
$sourceRepository->handleFilter('path', $path);
|
||||
$destinationRepository->handleFilter('path', $path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validator for options.
|
||||
*
|
||||
* @return Validator
|
||||
*/
|
||||
protected function validator(): Validator
|
||||
{
|
||||
$fs = Storage::disk($this->disk());
|
||||
|
||||
return \Illuminate\Support\Facades\Validator::make($this->options(), [
|
||||
'path' => ['nullable', 'string', 'doesnt_start_with:/', new StorageDirectoryExistsRule($fs)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Wiki\Video;
|
||||
|
||||
use App\Concerns\Repositories\Wiki\ReconcilesVideoRepositories;
|
||||
use App\Models\BaseModel;
|
||||
use App\Actions\Repositories\ReconcileRepositories;
|
||||
use App\Actions\Repositories\Wiki\Video\ReconcileVideoRepositories;
|
||||
use App\Console\Commands\Wiki\StorageReconcileCommand;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
|
||||
use App\Repositories\Service\DigitalOcean\VideoRepository as VideoSourceRepository;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Repositories\Storage\Wiki\VideoRepository as VideoSourceRepository;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* Class VideoReconcileCommand.
|
||||
*/
|
||||
class VideoReconcileCommand extends Command
|
||||
class VideoReconcileCommand extends StorageReconcileCommand
|
||||
{
|
||||
use ReconcilesVideoRepositories;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
@@ -36,136 +35,44 @@ class VideoReconcileCommand extends Command
|
||||
protected $description = 'Perform set reconciliation between object storage and video database';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* Get the name of the disk that represents the filesystem.
|
||||
*
|
||||
* @return int
|
||||
* @return string
|
||||
*/
|
||||
public function handle(): int
|
||||
protected function disk(): string
|
||||
{
|
||||
$sourceRepository = App::make(VideoSourceRepository::class);
|
||||
|
||||
$destinationRepository = App::make(VideoDestinationRepository::class);
|
||||
|
||||
$path = $this->option('path');
|
||||
if ($path !== null) {
|
||||
if (! $sourceRepository->validateFilter('path', $path) || ! $destinationRepository->validateFilter('path', $path)) {
|
||||
$this->error("Invalid path '$path'");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$sourceRepository->handleFilter('path', $path);
|
||||
$destinationRepository->handleFilter('path', $path);
|
||||
}
|
||||
|
||||
$this->reconcileRepositories($sourceRepository, $destinationRepository);
|
||||
|
||||
return 0;
|
||||
return Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the result to console and log the results to the app log.
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return void
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function postReconciliationTask(): void
|
||||
protected function getSourceRepository(array $validated): ?RepositoryInterface
|
||||
{
|
||||
if ($this->hasResults()) {
|
||||
if ($this->hasChanges()) {
|
||||
Log::info("$this->created Videos created, $this->deleted Videos deleted, $this->updated Videos updated");
|
||||
$this->info("$this->created Videos created, $this->deleted Videos deleted, $this->updated Videos updated");
|
||||
}
|
||||
if ($this->hasFailures()) {
|
||||
Log::error("Failed to create $this->createdFailed Videos, delete $this->deletedFailed Videos, update $this->updatedFailed Videos");
|
||||
$this->error("Failed to create $this->createdFailed Videos, delete $this->deletedFailed Videos, update $this->updatedFailed Videos");
|
||||
}
|
||||
} else {
|
||||
Log::info('No Videos created or deleted or updated');
|
||||
$this->info('No Videos created or deleted or updated');
|
||||
}
|
||||
return App::make(VideoSourceRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful video creation.
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
* @param array $validated
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function handleCreated(BaseModel $model): void
|
||||
protected function getDestinationRepository(array $validated): ?RepositoryInterface
|
||||
{
|
||||
Log::info("Video '{$model->getName()}' created");
|
||||
$this->info("Video '{$model->getName()}' created");
|
||||
return App::make(VideoDestinationRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed video creation.
|
||||
* Get the reconciliation action.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
* @return ReconcileRepositories
|
||||
*/
|
||||
protected function handleFailedCreation(BaseModel $model): void
|
||||
protected function getAction(): ReconcileRepositories
|
||||
{
|
||||
Log::error("Video '{$model->getName()}' was not created");
|
||||
$this->error("Video '{$model->getName()}' was not created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful video deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleDeleted(BaseModel $model): void
|
||||
{
|
||||
Log::info("Video '{$model->getName()}' deleted");
|
||||
$this->info("Video '{$model->getName()}' deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed video deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedDeletion(BaseModel $model): void
|
||||
{
|
||||
Log::error("Video '{$model->getName()}' was not deleted");
|
||||
$this->error("Video '{$model->getName()}' was not deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful video update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleUpdated(BaseModel $model): void
|
||||
{
|
||||
Log::info("Video '{$model->getName()}' updated");
|
||||
$this->info("Video '{$model->getName()}' updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed video update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedUpdate(BaseModel $model): void
|
||||
{
|
||||
Log::error("Video '{$model->getName()}' was not updated");
|
||||
$this->error("Video '{$model->getName()}' was not updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for exception.
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*/
|
||||
protected function handleException(Exception $exception): void
|
||||
{
|
||||
Log::error($exception->getMessage());
|
||||
$this->error($exception->getMessage());
|
||||
return new ReconcileVideoRepositories();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Wiki\Anime\AnimeSynonym;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Models\Wiki\Audio;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Series;
|
||||
@@ -25,6 +26,7 @@ use App\Pivots\ArtistImage;
|
||||
use App\Pivots\ArtistMember;
|
||||
use App\Pivots\ArtistResource;
|
||||
use App\Pivots\ArtistSong;
|
||||
use App\Pivots\StudioImage;
|
||||
use App\Pivots\StudioResource;
|
||||
|
||||
/**
|
||||
@@ -37,7 +39,23 @@ class WikiDatabaseDumpCommand extends DatabaseDumpCommand
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'db:dump-wiki {--C|create : Whether the dumper should include create table statements}';
|
||||
protected $signature = 'db:dump-wiki
|
||||
{--comments : Write additional information in the MySQL dump such as program version, server version and host}
|
||||
{--data-only : Dump only the data without the schema in PostgreSQL dump}
|
||||
{--default-character-set=utf8 : Specify default character set in MySQL dump}
|
||||
{--extended-insert : Use multiple-row insert syntax in MySQL dump}
|
||||
{--inserts : Dump data as INSERT commands rather than COPY in PostgreSQL dump}
|
||||
{--lock-tables : Lock all tables before dumping them to MySQL dump}
|
||||
{--no-create-info : Turn off CREATE TABLE statements in MySQL dump}
|
||||
{--quick : Retrieve rows for a table from the server one row at a time in MySQL dump}
|
||||
{--set-gtid-purged=AUTO : Add SET GTID_PURGED to output in MySQL dump}
|
||||
{--single-transaction : Issue a BEGIN SQL statement before dumping data from server for MySQL dump}
|
||||
{--skip-column-statistics : Turn off ANALYZE table statements in the MySQL dump}
|
||||
{--skip-comments : Do not write additional information in the MySQL dump}
|
||||
{--skip-extended-insert : Turn off extended-insert in MySQL dump}
|
||||
{--skip-lock-tables : Turn off locking tables before dumping to MySQL dump}
|
||||
{--skip-quick : Do not retrieve rows for a table from the server one row at a time in MySQL dump}
|
||||
';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -68,11 +86,13 @@ class WikiDatabaseDumpCommand extends DatabaseDumpCommand
|
||||
ArtistMember::TABLE,
|
||||
ArtistResource::TABLE,
|
||||
ArtistSong::TABLE,
|
||||
Audio::TABLE,
|
||||
ExternalResource::TABLE,
|
||||
Image::TABLE,
|
||||
Series::TABLE,
|
||||
Song::TABLE,
|
||||
Studio::TABLE,
|
||||
StudioImage::TABLE,
|
||||
StudioResource::TABLE,
|
||||
Video::TABLE,
|
||||
];
|
||||
|
||||
+14
-8
@@ -15,6 +15,7 @@ use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Database\Console\PruneCommand as PruneModelsCommand;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Queue\Console\PruneFailedJobsCommand;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Laravel\Horizon\Console\SnapshotCommand;
|
||||
use Laravel\Sanctum\Console\Commands\PruneExpired;
|
||||
use Laravel\Telescope\Console\PruneCommand as PruneTelescopeEntriesCommand;
|
||||
@@ -40,19 +41,22 @@ class Kernel extends ConsoleKernel
|
||||
->storeOutput()
|
||||
->hourly();
|
||||
|
||||
$schedule->command(DocumentDatabaseDumpCommand::class)
|
||||
// Managed database requires --single-transaction and --set-gtid-purged=OFF
|
||||
$schedule->command(DocumentDatabaseDumpCommand::class, ['--single-transaction', '--set-gtid-purged' => 'OFF'])
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->storeOutput()
|
||||
->daily();
|
||||
|
||||
$schedule->command(WikiDatabaseDumpCommand::class)
|
||||
// Managed database requires --single-transaction and --set-gtid-purged=OFF
|
||||
$schedule->command(WikiDatabaseDumpCommand::class, ['--single-transaction', '--set-gtid-purged' => 'OFF'])
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->storeOutput()
|
||||
->daily();
|
||||
|
||||
$schedule->command(WikiDatabaseDumpCommand::class, ['--create'])
|
||||
// Managed database requires --single-transaction and --set-gtid-purged=OFF
|
||||
$schedule->command(WikiDatabaseDumpCommand::class, ['--single-transaction', '--set-gtid-purged' => 'OFF', '--no-create-info'])
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->storeOutput()
|
||||
@@ -82,11 +86,13 @@ class Kernel extends ConsoleKernel
|
||||
->storeOutput()
|
||||
->daily();
|
||||
|
||||
$schedule->command(PruneTelescopeEntriesCommand::class)
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->storeOutput()
|
||||
->daily();
|
||||
if (Config::bool('telescope.enabled')) {
|
||||
$schedule->command(PruneTelescopeEntriesCommand::class)
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->storeOutput()
|
||||
->daily();
|
||||
}
|
||||
|
||||
$schedule->command(SnapshotCommand::class)
|
||||
->withoutOverlapping()
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Constants\Config;
|
||||
|
||||
/**
|
||||
* Class AudioConstants.
|
||||
*/
|
||||
class AudioConstants
|
||||
{
|
||||
final public const DEFAULT_DISK_QUALIFIED = 'audio.default_disk';
|
||||
|
||||
final public const DISKS_QUALIFIED = 'audio.disks';
|
||||
|
||||
final public const NGINX_REDIRECT_QUALIFIED = 'audio.nginx_redirect';
|
||||
|
||||
final public const PATH_QUALIFIED = 'audio.path';
|
||||
|
||||
final public const STREAMING_METHOD_QUALIFIED = 'audio.streaming_method';
|
||||
|
||||
final public const URL_QUALIFIED = 'audio.url';
|
||||
}
|
||||
@@ -13,6 +13,10 @@ class FlagConstants
|
||||
|
||||
final public const ALLOW_VIDEO_STREAMS_FLAG_QUALIFIED = 'flags.allow_video_streams';
|
||||
|
||||
final public const ALLOW_AUDIO_STREAMS_FLAG = 'allow_audio_streams';
|
||||
|
||||
final public const ALLOW_AUDIO_STREAMS_FLAG_QUALIFIED = 'flags.allow_audio_streams';
|
||||
|
||||
final public const ALLOW_DISCORD_NOTIFICATIONS_FLAG = 'allow_discord_notifications';
|
||||
|
||||
final public const ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED = 'flags.allow_discord_notifications';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Constants\Config;
|
||||
|
||||
/**
|
||||
* Class VideoConstants.
|
||||
*/
|
||||
class VideoConstants
|
||||
{
|
||||
final public const DEFAULT_DISK_QUALIFIED = 'video.default_disk';
|
||||
|
||||
final public const DISKS_QUALIFIED = 'video.disks';
|
||||
|
||||
final public const ENCODER_VERSION = 'encoder_version';
|
||||
|
||||
final public const ENCODER_VERSION_QUALIFIED = 'video.encoder_version';
|
||||
|
||||
final public const NGINX_REDIRECT_QUALIFIED = 'video.nginx_redirect';
|
||||
|
||||
final public const PATH_QUALIFIED = 'video.path';
|
||||
|
||||
final public const STREAMING_METHOD_QUALIFIED = 'video.streaming_method';
|
||||
|
||||
final public const URL_QUALIFIED = 'video.url';
|
||||
}
|
||||
@@ -9,9 +9,13 @@ namespace App\Constants\Config;
|
||||
*/
|
||||
class WikiConstants
|
||||
{
|
||||
final public const FEATURED_ENTRY_SETTING = 'featured_entry';
|
||||
|
||||
final public const FEATURED_ENTRY_SETTING_QUALIFIED = 'wiki.featured_entry';
|
||||
|
||||
final public const FEATURED_THEME_SETTING = 'featured_theme';
|
||||
|
||||
final public const FEATURED_VIDEO_SETTING = 'featured_video';
|
||||
|
||||
final public const FEATURED_VIDEO_SETTING_QUALIFIED = 'wiki.featured_video';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contracts\Models;
|
||||
|
||||
/**
|
||||
* Interface Streamable.
|
||||
*/
|
||||
interface Streamable
|
||||
{
|
||||
/**
|
||||
* Get the path of the streamable model in the filesystem.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function path(): string;
|
||||
|
||||
/**
|
||||
* Get the basename of the streamable model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function basename(): string;
|
||||
|
||||
/**
|
||||
* Get the MIME type / content type of the streamable model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mimetype(): string;
|
||||
|
||||
/**
|
||||
* Get the content length of the streamable model.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function size(): int;
|
||||
}
|
||||
@@ -8,9 +8,9 @@ use App\Models\Auth\User;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Interface Pipe.
|
||||
* Interface PipeInterface.
|
||||
*/
|
||||
interface Pipe
|
||||
interface PipeInterface
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
+4
-11
@@ -8,9 +8,11 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Interface Repository.
|
||||
* Interface RepositoryInterface.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
interface Repository
|
||||
interface RepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Get models from the repository.
|
||||
@@ -45,15 +47,6 @@ interface Repository
|
||||
*/
|
||||
public function update(Model $model, array $attributes): bool;
|
||||
|
||||
/**
|
||||
* Validate repository filter.
|
||||
*
|
||||
* @param string $filter
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function validateFilter(string $filter, mixed $value = null): bool;
|
||||
|
||||
/**
|
||||
* Filter repository models.
|
||||
*
|
||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Discord;
|
||||
|
||||
use App\Enums\Http\Api\Filter\AllowedDateFormat;
|
||||
use BenSampo\Enum\Enum;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Support\Str;
|
||||
use JsonSerializable;
|
||||
@@ -75,6 +77,11 @@ class DiscordEmbedField implements Arrayable, JsonSerializable
|
||||
return $value->description;
|
||||
}
|
||||
|
||||
// Use 'Y-m-d' format for dates
|
||||
if ($value instanceof Carbon) {
|
||||
return $value->format(AllowedDateFormat::YMD);
|
||||
}
|
||||
|
||||
// Pretty print booleans
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Actions;
|
||||
|
||||
use App\Enums\BaseEnum;
|
||||
|
||||
/**
|
||||
* Class ActionStatus.
|
||||
*
|
||||
* @method static static PASSED()
|
||||
* @method static static FAILED()
|
||||
* @method static static SKIPPED()
|
||||
*/
|
||||
final class ActionStatus extends BaseEnum
|
||||
{
|
||||
public const PASSED = 0;
|
||||
public const FAILED = 1;
|
||||
public const SKIPPED = 2;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Actions\Models\Wiki\Video;
|
||||
|
||||
use App\Enums\BaseEnum;
|
||||
|
||||
/**
|
||||
* Class DeriveSourceVideo.
|
||||
*
|
||||
* @method static static YES()
|
||||
* @method static static NO()
|
||||
*/
|
||||
final class DeriveSourceVideo extends BaseEnum
|
||||
{
|
||||
public const YES = 0;
|
||||
public const NO = 1;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Actions\Models\Wiki\Video;
|
||||
|
||||
use App\Enums\BaseEnum;
|
||||
|
||||
/**
|
||||
* Class OverwriteAudio.
|
||||
*
|
||||
* @method static static YES()
|
||||
* @method static static NO()
|
||||
*/
|
||||
final class OverwriteAudio extends BaseEnum
|
||||
{
|
||||
public const YES = 0;
|
||||
public const NO = 1;
|
||||
}
|
||||
@@ -5,7 +5,10 @@ declare(strict_types=1);
|
||||
namespace App\Enums\Models\Wiki;
|
||||
|
||||
use App\Enums\BaseEnum;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@@ -50,8 +53,8 @@ final class ResourceSite extends BaseEnum
|
||||
self::TWITTER => 'twitter.com',
|
||||
self::ANIDB => 'anidb.net',
|
||||
self::ANILIST => 'anilist.co',
|
||||
self::ANIME_PLANET => 'anime-planet.com',
|
||||
self::ANN => 'animenewsnetwork.com',
|
||||
self::ANIME_PLANET => 'www.anime-planet.com',
|
||||
self::ANN => 'www.animenewsnetwork.com',
|
||||
self::KITSU => 'kitsu.io',
|
||||
self::MAL => 'myanimelist.net',
|
||||
self::WIKI => 'wikipedia.org',
|
||||
@@ -90,10 +93,77 @@ final class ResourceSite extends BaseEnum
|
||||
ResourceSite::ANILIST,
|
||||
ResourceSite::ANN,
|
||||
ResourceSite::MAL => Str::match('/\d+/', $link),
|
||||
ResourceSite::ANIME_PLANET => self::parseAnimePlanetIdFromLink($link),
|
||||
ResourceSite::KITSU => self::parseKitsuIdFromLink($link),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse Anime Planet ID from link.
|
||||
*
|
||||
* @param string $link
|
||||
* @return string|null
|
||||
*/
|
||||
protected static function parseAnimePlanetIdFromLink(string $link): ?string
|
||||
{
|
||||
// We only want to attempt to parse the ID for an anime resource
|
||||
if (Str::match('/^https:\/\/www\.anime-planet\.com\/anime\/[a-zA-Z0-9-]+$/', $link) !== $link) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::get($link)
|
||||
->throw()
|
||||
->body();
|
||||
|
||||
return Str::match(
|
||||
'/["\']?ENTRY_INFO["\']? *: *{.*id["\']? *: *["\']?(\d+)["\']? *,/s',
|
||||
$response
|
||||
);
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse Kitsu ID from link.
|
||||
*
|
||||
* @param string $link
|
||||
* @return string|null
|
||||
*/
|
||||
protected static function parseKitsuIdFromLink(string $link): ?string
|
||||
{
|
||||
try {
|
||||
$query = '
|
||||
query ($slug: String!) {
|
||||
findAnimeBySlug(slug: $slug) {
|
||||
id
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'slug' => Str::afterLast($link, '/'),
|
||||
];
|
||||
|
||||
$response = Http::post('https://kitsu.io/api/graphql', [
|
||||
'query' => $query,
|
||||
'variables' => $variables,
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
return Arr::get($response, 'data.findAnimeBySlug.id');
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of the site for anime resources.
|
||||
*
|
||||
@@ -107,8 +177,8 @@ final class ResourceSite extends BaseEnum
|
||||
return match ($site->value) {
|
||||
ResourceSite::ANIDB => "https://anidb.net/anime/$id/",
|
||||
ResourceSite::ANILIST => "https://anilist.co/anime/$id/",
|
||||
ResourceSite::ANIME_PLANET => "https://anime-planet.com/anime/$slug",
|
||||
ResourceSite::ANN => "https://animenewsnetwork.com/encyclopedia/anime.php?id=$id",
|
||||
ResourceSite::ANIME_PLANET => "https://www.anime-planet.com/anime/$slug",
|
||||
ResourceSite::ANN => "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=$id",
|
||||
ResourceSite::KITSU => "https://kitsu.io/anime/$slug",
|
||||
ResourceSite::MAL => "https://myanimelist.net/anime/$id/",
|
||||
default => null,
|
||||
@@ -128,8 +198,8 @@ final class ResourceSite extends BaseEnum
|
||||
return match ($site->value) {
|
||||
ResourceSite::ANIDB => "https://anidb.net/creator/$id/",
|
||||
ResourceSite::ANILIST => "https://anilist.co/studio/$id/",
|
||||
ResourceSite::ANIME_PLANET => "https://anime-planet.com/anime/studios/$slug",
|
||||
ResourceSite::ANN => "https://animenewsnetwork.com/encyclopedia/company.php?id=$id",
|
||||
ResourceSite::ANIME_PLANET => "https://www.anime-planet.com/anime/studios/$slug",
|
||||
ResourceSite::ANN => "https://www.animenewsnetwork.com/encyclopedia/company.php?id=$id",
|
||||
ResourceSite::MAL => "https://myanimelist.net/anime/producer/$id/",
|
||||
default => null,
|
||||
};
|
||||
|
||||
@@ -17,4 +17,23 @@ final class VideoSource extends BaseEnum
|
||||
public const DVD = 3;
|
||||
public const VHS = 4;
|
||||
public const LD = 5;
|
||||
|
||||
/**
|
||||
* Score sources to help prioritize videos.
|
||||
*
|
||||
* @param int|null $value
|
||||
* @return int
|
||||
*/
|
||||
public static function getPriority(?int $value): int
|
||||
{
|
||||
return match ($value) {
|
||||
self::BD => 60,
|
||||
self::DVD => 50,
|
||||
self::LD => 40,
|
||||
self::VHS => 30,
|
||||
self::WEB => 20,
|
||||
self::RAW => 10,
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Admin\Setting;
|
||||
|
||||
use App\Constants\Config\ServiceConstants;
|
||||
use App\Contracts\Events\DiscordMessageEvent;
|
||||
use App\Enums\Discord\EmbedColor;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use NotificationChannels\Discord\DiscordMessage;
|
||||
|
||||
/**
|
||||
* Class SettingCreated.
|
||||
*/
|
||||
class SettingCreated extends SettingEvent implements DiscordMessageEvent
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Get Discord message payload.
|
||||
*
|
||||
* @return DiscordMessage
|
||||
*/
|
||||
public function getDiscordMessage(): DiscordMessage
|
||||
{
|
||||
$setting = $this->getSetting();
|
||||
|
||||
return DiscordMessage::create('', [
|
||||
'description' => "Setting '**{$setting->getName()}**' has been created.",
|
||||
'color' => EmbedColor::GREEN,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Discord channel the message will be sent to.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDiscordChannel(): string
|
||||
{
|
||||
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Admin\Setting;
|
||||
|
||||
use App\Constants\Config\ServiceConstants;
|
||||
use App\Contracts\Events\DiscordMessageEvent;
|
||||
use App\Enums\Discord\EmbedColor;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use NotificationChannels\Discord\DiscordMessage;
|
||||
|
||||
/**
|
||||
* Class SettingDeleted.
|
||||
*/
|
||||
class SettingDeleted extends SettingEvent implements DiscordMessageEvent
|
||||
{
|
||||
use Dispatchable;
|
||||
|
||||
/**
|
||||
* Get Discord message payload.
|
||||
*
|
||||
* @return DiscordMessage
|
||||
*/
|
||||
public function getDiscordMessage(): DiscordMessage
|
||||
{
|
||||
$setting = $this->getSetting();
|
||||
|
||||
return DiscordMessage::create('', [
|
||||
'description' => "Setting '**{$setting->getName()}**' has been deleted.",
|
||||
'color' => EmbedColor::RED,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Discord channel the message will be sent to.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDiscordChannel(): string
|
||||
{
|
||||
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Admin\Setting;
|
||||
|
||||
use App\Models\Admin\Setting;
|
||||
|
||||
/**
|
||||
* Class SettingEvent.
|
||||
*/
|
||||
abstract class SettingEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Setting $setting
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected Setting $setting)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the setting that has fired this event.
|
||||
*
|
||||
* @return Setting
|
||||
*/
|
||||
public function getSetting(): Setting
|
||||
{
|
||||
return $this->setting;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Admin\Setting;
|
||||
|
||||
use App\Concerns\Discord\HasAttributeUpdateEmbedFields;
|
||||
use App\Constants\Config\ServiceConstants;
|
||||
use App\Contracts\Events\DiscordMessageEvent;
|
||||
use App\Enums\Discord\EmbedColor;
|
||||
use App\Models\Admin\Setting;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use NotificationChannels\Discord\DiscordMessage;
|
||||
|
||||
/**
|
||||
* Class SettingUpdated.
|
||||
*/
|
||||
class SettingUpdated extends SettingEvent implements DiscordMessageEvent
|
||||
{
|
||||
use Dispatchable;
|
||||
use HasAttributeUpdateEmbedFields;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Setting $setting
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Setting $setting)
|
||||
{
|
||||
parent::__construct($setting);
|
||||
$this->initializeEmbedFields($setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Discord message payload.
|
||||
*
|
||||
* @return DiscordMessage
|
||||
*/
|
||||
public function getDiscordMessage(): DiscordMessage
|
||||
{
|
||||
$setting = $this->getSetting();
|
||||
|
||||
return DiscordMessage::create('', [
|
||||
'description' => "Setting '**{$setting->getName()}**' has been updated.",
|
||||
'fields' => $this->getEmbedFields(),
|
||||
'color' => EmbedColor::YELLOW,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Discord channel the message will be sent to.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDiscordChannel(): string
|
||||
{
|
||||
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class TransactionUpdated extends AdminUpdatedEvent
|
||||
public function __construct(Transaction $transaction)
|
||||
{
|
||||
parent::__construct($transaction);
|
||||
$this->initializeEmbedFields($transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Pivot\StudioImage;
|
||||
|
||||
use App\Events\Base\Pivot\PivotCreatedEvent;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\StudioImage;
|
||||
|
||||
/**
|
||||
* Class StudioImageCreated.
|
||||
*
|
||||
* @extends PivotCreatedEvent<Studio, Image>
|
||||
*/
|
||||
class StudioImageCreated extends PivotCreatedEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param StudioImage $studioImage
|
||||
*/
|
||||
public function __construct(StudioImage $studioImage)
|
||||
{
|
||||
parent::__construct($studioImage->studio, $studioImage->image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description for the Discord message payload.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDiscordMessageDescription(): string
|
||||
{
|
||||
$foreign = $this->getForeign();
|
||||
$related = $this->getRelated();
|
||||
|
||||
return "Image '**{$foreign->getName()}**' has been attached to Studio '**{$related->getName()}**'.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Pivot\StudioImage;
|
||||
|
||||
use App\Events\Base\Pivot\PivotDeletedEvent;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\StudioImage;
|
||||
|
||||
/**
|
||||
* Class StudioImageDeleted.
|
||||
*
|
||||
* @extends PivotDeletedEvent<Studio, Image>
|
||||
*/
|
||||
class StudioImageDeleted extends PivotDeletedEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param StudioImage $studioImage
|
||||
*/
|
||||
public function __construct(StudioImage $studioImage)
|
||||
{
|
||||
parent::__construct($studioImage->studio, $studioImage->image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description for the Discord message payload.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDiscordMessageDescription(): string
|
||||
{
|
||||
$foreign = $this->getForeign();
|
||||
$related = $this->getRelated();
|
||||
|
||||
return "Image '**{$foreign->getName()}**' has been detached from Studio '**{$related->getName()}**'.";
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Wiki\Anime\Theme;
|
||||
|
||||
use App\Events\BaseEvent;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
|
||||
/**
|
||||
* Class ThemeCreating.
|
||||
*
|
||||
* @extends BaseEvent<AnimeTheme>
|
||||
*/
|
||||
class ThemeCreating extends BaseEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param AnimeTheme $theme
|
||||
*/
|
||||
public function __construct(AnimeTheme $theme)
|
||||
{
|
||||
parent::__construct($theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model that has fired this event.
|
||||
*
|
||||
* @return AnimeTheme
|
||||
*/
|
||||
public function getModel(): AnimeTheme
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Wiki\Audio;
|
||||
|
||||
use App\Events\Base\Wiki\WikiCreatedEvent;
|
||||
use App\Models\Wiki\Audio;
|
||||
|
||||
/**
|
||||
* Class AudioCreated.
|
||||
*
|
||||
* @extends WikiCreatedEvent<Audio>
|
||||
*/
|
||||
class AudioCreated extends WikiCreatedEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Audio $audio
|
||||
*/
|
||||
public function __construct(Audio $audio)
|
||||
{
|
||||
parent::__construct($audio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model that has fired this event.
|
||||
*
|
||||
* @return Audio
|
||||
*/
|
||||
public function getModel(): Audio
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description for the Discord message payload.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDiscordMessageDescription(): string
|
||||
{
|
||||
return "Audio '**{$this->getModel()->getName()}**' has been created.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Wiki\Audio;
|
||||
|
||||
use App\Events\Base\Wiki\WikiDeletedEvent;
|
||||
use App\Models\Wiki\Audio;
|
||||
use App\Nova\Resources\Wiki\Audio as AudioResource;
|
||||
|
||||
/**
|
||||
* Class AudioDeleted.
|
||||
*
|
||||
* @extends WikiDeletedEvent<Audio>
|
||||
*/
|
||||
class AudioDeleted extends WikiDeletedEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Audio $audio
|
||||
*/
|
||||
public function __construct(Audio $audio)
|
||||
{
|
||||
parent::__construct($audio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model that has fired this event.
|
||||
*
|
||||
* @return Audio
|
||||
*/
|
||||
public function getModel(): Audio
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description for the Discord message payload.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDiscordMessageDescription(): string
|
||||
{
|
||||
return "Audio '**{$this->getModel()->getName()}**' has been deleted.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message for the nova notification.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getNotificationMessage(): string
|
||||
{
|
||||
return "Audio '{$this->getModel()->getName()}' has been deleted. It will be automatically pruned in one week. Please review.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the nova notification.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getNotificationUrl(): string
|
||||
{
|
||||
$uriKey = AudioResource::uriKey();
|
||||
|
||||
return "/resources/$uriKey/{$this->getModel()->getKey()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Wiki\Audio;
|
||||
|
||||
use App\Events\Base\Wiki\WikiRestoredEvent;
|
||||
use App\Models\Wiki\Audio;
|
||||
|
||||
/**
|
||||
* Class AudioRestored.
|
||||
*
|
||||
* @extends WikiRestoredEvent<Audio>
|
||||
*/
|
||||
class AudioRestored extends WikiRestoredEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Audio $audio
|
||||
*/
|
||||
public function __construct(Audio $audio)
|
||||
{
|
||||
parent::__construct($audio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model that has fired this event.
|
||||
*
|
||||
* @return Audio
|
||||
*/
|
||||
public function getModel(): Audio
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description for the Discord message payload.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDiscordMessageDescription(): string
|
||||
{
|
||||
return "Audio '**{$this->getModel()->getName()}**' has been restored.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Wiki\Audio;
|
||||
|
||||
use App\Events\Base\Wiki\WikiUpdatedEvent;
|
||||
use App\Models\Wiki\Audio;
|
||||
|
||||
/**
|
||||
* Class AudioUpdated.
|
||||
*
|
||||
* @extends WikiUpdatedEvent<Audio>
|
||||
*/
|
||||
class AudioUpdated extends WikiUpdatedEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Audio $audio
|
||||
*/
|
||||
public function __construct(Audio $audio)
|
||||
{
|
||||
parent::__construct($audio);
|
||||
$this->initializeEmbedFields($audio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model that has fired this event.
|
||||
*
|
||||
* @return Audio
|
||||
*/
|
||||
public function getModel(): Audio
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description for the Discord message payload.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDiscordMessageDescription(): string
|
||||
{
|
||||
return "Audio '**{$this->getModel()->getName()}**' has been updated.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Config\Flags;
|
||||
|
||||
use App\Constants\Config\FlagConstants;
|
||||
use App\Http\Api\Field\Field;
|
||||
|
||||
/**
|
||||
* Class FlagsAllowAudioStreamsField.
|
||||
*/
|
||||
class FlagsAllowAudioStreamsField extends Field
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(FlagConstants::ALLOW_AUDIO_STREAMS_FLAG);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Pivot\AnimeImage;
|
||||
|
||||
use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Contracts\Http\Api\Field\SelectableField;
|
||||
use App\Http\Api\Criteria\Field\Criteria;
|
||||
use App\Http\Api\Field\Field;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Pivots\AnimeImage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Class AnimeImageAnimeIdField.
|
||||
*/
|
||||
class AnimeImageAnimeIdField extends Field implements CreatableField, SelectableField
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(AnimeImage::ATTRIBUTE_ANIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists(Anime::TABLE, Anime::ATTRIBUTE_ID),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be included in the select clause of our query.
|
||||
*
|
||||
* @param Criteria|null $criteria
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldSelect(?Criteria $criteria): bool
|
||||
{
|
||||
// Needed to match anime relation.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Pivot\AnimeImage;
|
||||
|
||||
use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Contracts\Http\Api\Field\SelectableField;
|
||||
use App\Http\Api\Criteria\Field\Criteria;
|
||||
use App\Http\Api\Field\Field;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pivots\AnimeImage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Class AnimeImageImageIdField.
|
||||
*/
|
||||
class AnimeImageImageIdField extends Field implements CreatableField, SelectableField
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(AnimeImage::ATTRIBUTE_IMAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists(Image::TABLE, Image::ATTRIBUTE_ID),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be included in the select clause of our query.
|
||||
*
|
||||
* @param Criteria|null $criteria
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldSelect(?Criteria $criteria): bool
|
||||
{
|
||||
// Needed to match image relation.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Wiki\Anime\Theme;
|
||||
|
||||
use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Contracts\Http\Api\Field\UpdatableField;
|
||||
use App\Http\Api\Field\StringField;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
@@ -12,7 +13,7 @@ use Illuminate\Http\Request;
|
||||
/**
|
||||
* Class ThemeGroupField.
|
||||
*/
|
||||
class ThemeSlugField extends StringField implements UpdatableField
|
||||
class ThemeSlugField extends StringField implements CreatableField, UpdatableField
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
@@ -22,6 +23,22 @@ class ThemeSlugField extends StringField implements UpdatableField
|
||||
parent::__construct(AnimeTheme::ATTRIBUTE_SLUG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'string',
|
||||
'max:192',
|
||||
'alpha_dash',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the update validation rules for the field.
|
||||
*
|
||||
@@ -33,6 +50,7 @@ class ThemeSlugField extends StringField implements UpdatableField
|
||||
return [
|
||||
'sometimes',
|
||||
'required',
|
||||
'string',
|
||||
'max:192',
|
||||
'alpha_dash',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Wiki\Audio;
|
||||
|
||||
use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Http\Api\Criteria\Field\Criteria;
|
||||
use App\Http\Api\Field\StringField;
|
||||
use App\Http\Resources\Wiki\Resource\AudioResource;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Class AudioBasenameField.
|
||||
*/
|
||||
class AudioBasenameField extends StringField implements CreatableField
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Audio::ATTRIBUTE_BASENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'string',
|
||||
'max:192',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be included in the select clause of our query.
|
||||
*
|
||||
* @param Criteria|null $criteria
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldSelect(?Criteria $criteria): bool
|
||||
{
|
||||
// The link field is dependent on this field to build the route.
|
||||
return parent::shouldSelect($criteria) || $criteria->isAllowedField(AudioResource::ATTRIBUTE_LINK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Wiki\Audio;
|
||||
|
||||
use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Http\Api\Field\StringField;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Class AudioFilenameField.
|
||||
*/
|
||||
class AudioFilenameField extends StringField implements CreatableField
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Audio::ATTRIBUTE_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'string',
|
||||
'max:192',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Wiki\Audio;
|
||||
|
||||
use App\Http\Api\Field\Field;
|
||||
use App\Http\Resources\Wiki\Resource\AudioResource;
|
||||
|
||||
/**
|
||||
* Class AudioLinkField.
|
||||
*/
|
||||
class AudioLinkField extends Field
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(AudioResource::ATTRIBUTE_LINK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Wiki\Audio;
|
||||
|
||||
use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Http\Api\Field\StringField;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Class AudioMimeTypeField.
|
||||
*/
|
||||
class AudioMimeTypeField extends StringField implements CreatableField
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Audio::ATTRIBUTE_MIMETYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'string',
|
||||
'max:192',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Wiki\Audio;
|
||||
|
||||
use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Http\Api\Field\StringField;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Class AudioPathField.
|
||||
*/
|
||||
class AudioPathField extends StringField implements CreatableField
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Audio::ATTRIBUTE_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'string',
|
||||
'max:192',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Wiki\Audio;
|
||||
|
||||
use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Http\Api\Field\IntField;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Class AudioSizeField.
|
||||
*/
|
||||
class AudioSizeField extends IntField implements CreatableField
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Audio::ATTRIBUTE_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'integer',
|
||||
'min:0',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Contracts\Http\Api\Field\UpdatableField;
|
||||
use App\Http\Api\Field\StringField;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Rules\Wiki\ResourceLinkMatchesSiteRule;
|
||||
use App\Rules\Wiki\Resource\ResourceLinkMatchesSiteRule;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\Contracts\Http\Api\Field\UpdatableField;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Http\Api\Field\EnumField;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Rules\Wiki\ResourceSiteMatchesLinkRule;
|
||||
use App\Rules\Wiki\Resource\ResourceSiteMatchesLinkRule;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Field\Wiki\Video;
|
||||
|
||||
use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Contracts\Http\Api\Field\SelectableField;
|
||||
use App\Contracts\Http\Api\Field\UpdatableField;
|
||||
use App\Http\Api\Criteria\Field\Criteria;
|
||||
use App\Http\Api\Field\Field;
|
||||
use App\Models\Wiki\Audio;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Class VideoAudioIdField.
|
||||
*/
|
||||
class VideoAudioIdField extends Field implements CreatableField, SelectableField, UpdatableField
|
||||
{
|
||||
/**
|
||||
* Create a new field instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Video::ATTRIBUTE_AUDIO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'sometimes',
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists(Audio::TABLE, Audio::ATTRIBUTE_ID),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be included in the select clause of our query.
|
||||
*
|
||||
* @param Criteria|null $criteria
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldSelect(?Criteria $criteria): bool
|
||||
{
|
||||
// Needed to match audio relation.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the update validation rules for the field.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function getUpdateRules(Request $request): array
|
||||
{
|
||||
return [
|
||||
'sometimes',
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists(Audio::TABLE, Audio::ATTRIBUTE_ID),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Query\Pivot\AnimeImage;
|
||||
|
||||
use App\Http\Api\Query\Base\EloquentReadQuery;
|
||||
use App\Http\Api\Schema\EloquentSchema;
|
||||
use App\Http\Api\Schema\Pivot\AnimeImageSchema;
|
||||
use App\Http\Resources\BaseCollection;
|
||||
use App\Http\Resources\BaseResource;
|
||||
use App\Http\Resources\Pivot\Collection\AnimeImageCollection;
|
||||
use App\Http\Resources\Pivot\Resource\AnimeImageResource;
|
||||
use App\Pivots\AnimeImage;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* Class AnimeImageReadQuery.
|
||||
*/
|
||||
class AnimeImageReadQuery extends EloquentReadQuery
|
||||
{
|
||||
/**
|
||||
* Get the resource schema.
|
||||
*
|
||||
* @return EloquentSchema
|
||||
*/
|
||||
public function schema(): EloquentSchema
|
||||
{
|
||||
return new AnimeImageSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query builder of the resource.
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
public function builder(): Builder
|
||||
{
|
||||
return AnimeImage::query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the json resource.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return BaseResource
|
||||
*/
|
||||
public function resource(mixed $resource): BaseResource
|
||||
{
|
||||
return new AnimeImageResource($resource, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource collection.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return BaseCollection
|
||||
*/
|
||||
public function collection(mixed $resource): BaseCollection
|
||||
{
|
||||
return new AnimeImageCollection($resource, $this);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user