feat(admin): migrate db dumps from github to platform (#465)

* feat: migrate encoding scripts from github to platform

* style: fix StyleCI finding

* fix: don't concatenate updated videos when attempting to attach entry on video upload

* fix(api): fix field dependencies & feat(admin): upload script from video detail screen
This commit is contained in:
paranarimasu
2022-09-24 00:01:37 -05:00
committed by GitHub
parent 99cf085666
commit 8b6b196c99
92 changed files with 5237 additions and 36 deletions
+15
View File
@@ -151,12 +151,22 @@ DUMP_STREAM_READS=
DUMP_DISABLE_ASSERTS=
DUMP_VISIBILITY=
SCRIPT_ACCESS_KEY_ID=
SCRIPT_SECRET_ACCESS_KEY=
SCRIPT_DEFAULT_REGION=
SCRIPT_ENDPOINT=
SCRIPT_BUCKET=
SCRIPT_STREAM_READS=
SCRIPT_DISABLE_ASSERTS=
SCRIPT_VISIBILITY=
# flags
ALLOW_VIDEO_STREAMS=false
ALLOW_AUDIO_STREAMS=false
ALLOW_DISCORD_NOTIFICATIONS=false
ALLOW_VIEW_RECORDING=false
ALLOW_DUMP_DOWNLOADING=false
ALLOW_SCRIPT_DOWNLOADING=false
# fortify
FORTIFY_PATH=
@@ -257,6 +267,11 @@ VIDEO_NGINX_REDIRECT=
VIDEO_ENCODER_VERSION=
VIDEO_UPLOAD_DISKS=
SCRIPT_DISK=scripts
SCRIPT_DISK_ROOT=
SCRIPT_URL=http://localhost
SCRIPT_PATH=
# web
WEB_URL=http://localhost
WEB_PATH=
@@ -139,6 +139,6 @@ abstract class ReconcileResults extends ActionResult
*/
protected function label(int|array|Countable $models = 1): string
{
return Str::plural(class_basename($this->model()), $models);
return Str::plural(Str::headline(class_basename($this->model())), $models);
}
}
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Actions\Repositories\Wiki\Video\Script;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\ReconcileResults;
use App\Models\Wiki\Video\VideoScript;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Class ReconcileScriptRepositoriesAction.
*
* @extends ReconcileRepositoriesAction<VideoScript>
*/
class ReconcileScriptRepositoriesAction extends ReconcileRepositoriesAction
{
/**
* The columns used for create and delete set operations.
*
* @return string[]
*/
protected function columnsForCreateDelete(): array
{
return [
VideoScript::ATTRIBUTE_ID,
VideoScript::ATTRIBUTE_PATH,
];
}
/**
* Callback for create and delete set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForCreateDelete(): Closure
{
return fn (VideoScript $first, VideoScript $second) => [$first->path] <=> [$second->path];
}
/**
* 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 ReconcileScriptResults($created, $deleted, $updated);
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Repositories\Wiki\Video\Script;
use App\Actions\Repositories\ReconcileResults;
use App\Models\Wiki\Video\VideoScript;
/**
* Class ReconcileScriptResults.
*
* @extends ReconcileResults<VideoScript>
*/
class ReconcileScriptResults extends ReconcileResults
{
/**
* Get the model of the reconciliation results.
*
* @return class-string<VideoScript>
*/
protected function model(): string
{
return VideoScript::class;
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Base\DeleteAction;
use App\Concerns\Repositories\Wiki\Video\ReconcilesScriptRepositories;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class DeleteScriptAction.
*
* @extends DeleteAction<VideoScript>
*/
class DeleteScriptAction extends DeleteAction
{
use ReconcilesScriptRepositories;
/**
* Create a new action instance.
*
* @param VideoScript $script
*/
public function __construct(VideoScript $script)
{
parent::__construct($script);
}
/**
* The list of disk names.
*
* @return array
*/
public function disks(): array
{
return Arr::wrap(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
}
/**
* Get the path to delete.
*
* @return string
*/
protected function path(): string
{
return $this->model->path;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Base\MoveAction;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class MoveVideoAction.
*
* @extends MoveAction<VideoScript>
*/
class MoveScriptAction extends MoveAction
{
/**
* Create a new action instance.
*
* @param VideoScript $script
* @param string $to
*/
public function __construct(VideoScript $script, string $to)
{
parent::__construct($script, $to);
}
/**
* The list of disk names.
*
* @return array
*/
public function disks(): array
{
return Arr::wrap(Config::get(VideoConstants::SCRIPT_DISK_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([
VideoScript::ATTRIBUTE_PATH => $this->to,
]);
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Video\Script;
use App\Actions\Repositories\ReconcileResults;
use App\Actions\Storage\Base\UploadAction;
use App\Concerns\Repositories\Wiki\Video\ReconcilesScriptRepositories;
use App\Constants\Config\VideoConstants;
use App\Contracts\Actions\Storage\StorageResults;
use App\Models\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
/**
* Class UploadScriptAction.
*/
class UploadScriptAction extends UploadAction
{
use ReconcilesScriptRepositories;
/**
* Create a new action instance.
*
* @param UploadedFile $file
* @param string $path
* @param Video|null $video
*/
public function __construct(UploadedFile $file, string $path, protected ?Video $video = null)
{
parent::__construct($file, $path);
}
/**
* Processes to be completed after handling action.
*
* @param StorageResults $storageResults
* @return void
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function then(StorageResults $storageResults): void
{
$reconcileResults = $this->reconcileRepositories();
$reconcileResults->toLog();
// The script was successfully uploaded and reconciled into the database, so we can attempt further actions
if ($reconcileResults instanceof ReconcileResults) {
$this->attachVideo($reconcileResults);
}
}
/**
* Attach video if uploaded from Upload Video Action.
*
* @param ReconcileResults $reconcileResults
* @return void
*/
protected function attachVideo(ReconcileResults $reconcileResults): void
{
$path = Str::of($this->path)
->finish('/')
->append($this->file->getClientOriginalName())
->__toString();
$script = $reconcileResults->getCreated()->firstWhere(VideoScript::ATTRIBUTE_PATH, $path);
if ($script instanceof VideoScript && $this->video !== null) {
$script->video()->associate($this->video)->save();
}
}
/**
* The list of disk names.
*
* @return array
*/
public function disks(): array
{
return Arr::wrap(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
}
}
@@ -6,6 +6,7 @@ namespace App\Actions\Storage\Wiki\Video;
use App\Actions\Repositories\ReconcileResults;
use App\Actions\Storage\Base\UploadAction;
use App\Actions\Storage\Wiki\Video\Script\UploadScriptAction;
use App\Concerns\Repositories\Wiki\ReconcilesVideoRepositories;
use App\Constants\Config\VideoConstants;
use App\Contracts\Actions\Storage\StorageResults;
@@ -27,9 +28,14 @@ class UploadVideoAction extends UploadAction
* @param UploadedFile $file
* @param string $path
* @param AnimeThemeEntry|null $entry
* @param UploadedFile|null $script
*/
public function __construct(UploadedFile $file, string $path, protected ?AnimeThemeEntry $entry = null)
{
public function __construct(
UploadedFile $file,
string $path,
protected ?AnimeThemeEntry $entry = null,
protected readonly ?UploadedFile $script = null
) {
parent::__construct($file, $path);
}
@@ -47,11 +53,48 @@ class UploadVideoAction extends UploadAction
$reconcileResults->toLog();
if ($reconcileResults instanceof ReconcileResults && $this->entry !== null) {
$video = $reconcileResults->getCreated()->firstWhere(Video::ATTRIBUTE_BASENAME, $this->file->getClientOriginalName());
if ($video instanceof Video) {
$video->animethemeentries()->attach($this->entry);
}
// The video was successfully uploaded and reconciled into the database, so we can attempt further actions
if ($reconcileResults instanceof ReconcileResults) {
$this->attachEntry($reconcileResults);
$this->uploadScript($reconcileResults);
}
}
/**
* Attach entry to created video if uploaded from entry detail screen.
*
* @param ReconcileResults $reconcileResults
* @return void
*/
protected function attachEntry(ReconcileResults $reconcileResults): void
{
$video = $reconcileResults->getCreated()->firstWhere(Video::ATTRIBUTE_BASENAME, $this->file->getClientOriginalName());
if ($video instanceof Video && $this->entry !== null) {
$video->animethemeentries()->attach($this->entry);
}
}
/**
* Upload & Associate Script if video upload was successful.
*
* @param ReconcileResults $reconcileResults
* @return void
*/
protected function uploadScript(ReconcileResults $reconcileResults): void
{
$video = $reconcileResults->getCreated()->firstWhere(Video::ATTRIBUTE_BASENAME, $this->file->getClientOriginalName());
if ($video === null) {
$video = $reconcileResults->getUpdated()->firstWhere(Video::ATTRIBUTE_BASENAME, $this->file->getClientOriginalName());
}
if ($video instanceof Video && $this->script !== null) {
$uploadScript = new UploadScriptAction($this->script, $this->path, $video);
$scriptResult = $uploadScript->handle();
$uploadScript->then($scriptResult);
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Wiki\Video;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\Wiki\Video\Script\ReconcileScriptRepositoriesAction;
use App\Contracts\Repositories\RepositoryInterface;
use App\Repositories\Eloquent\Wiki\Video\ScriptRepository as ScriptDestinationRepository;
use App\Repositories\Storage\Wiki\Video\ScriptRepository as ScriptSourceRepository;
use Illuminate\Support\Facades\App;
/**
* Trait ReconcilesScriptRepositories.
*/
trait ReconcilesScriptRepositories
{
/**
* Get source repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $data = []): ?RepositoryInterface
{
return App::make(ScriptSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
{
return App::make(ScriptDestinationRepository::class);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositoriesAction
*/
protected function action(): ReconcileRepositoriesAction
{
return new ReconcileScriptRepositoriesAction();
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Repositories\Storage\Wiki\Video;
use App\Concerns\Repositories\Wiki\Video\ReconcilesScriptRepositories;
use App\Console\Commands\Repositories\Storage\StorageReconcileCommand;
use App\Constants\Config\VideoConstants;
use Illuminate\Support\Facades\Config;
/**
* Class ScriptReconcileCommand.
*/
class ScriptReconcileCommand extends StorageReconcileCommand
{
use ReconcilesScriptRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:script
{--path= : The directory of scripts 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 script database';
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
}
}
@@ -39,6 +39,8 @@ abstract class StorageCommand extends Command
$storageResults->toLog();
$storageResults->toConsole($this);
$action->then($storageResults);
$result = $storageResults->toActionResult();
return $result->hasFailed() ? 1 : 0;
+4
View File
@@ -28,4 +28,8 @@ class FlagConstants
final public const ALLOW_DUMP_DOWNLOADING_FLAG = 'allow_dump_downloading';
final public const ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED = 'flags.allow_dump_downloading';
final public const ALLOW_SCRIPT_DOWNLOADING_FLAG = 'allow_script_downloading';
final public const ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED = 'flags.allow_script_downloading';
}
+6
View File
@@ -24,4 +24,10 @@ class VideoConstants
final public const STREAMING_METHOD_QUALIFIED = 'video.streaming_method';
final public const URL_QUALIFIED = 'video.url';
final public const SCRIPT_DISK_QUALIFIED = 'video.script.disk';
final public const SCRIPT_PATH_QUALIFIED = 'video.script.path';
final public const SCRIPT_URL_QUALIFIED = 'video.script.url';
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Wiki\Video\Script;
use App\Events\Base\Wiki\WikiCreatedEvent;
use App\Models\Wiki\Video\VideoScript;
/**
* Class VideoScriptCreated.
*
* @extends WikiCreatedEvent<VideoScript>
*/
class VideoScriptCreated extends WikiCreatedEvent
{
/**
* Create a new event instance.
*
* @param VideoScript $script
*/
public function __construct(VideoScript $script)
{
parent::__construct($script);
}
/**
* Get the model that has fired this event.
*
* @return VideoScript
*/
public function getModel(): VideoScript
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Script '**{$this->getModel()->getName()}**' has been created.";
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Events\Wiki\Video\Script;
use App\Events\Base\Wiki\WikiDeletedEvent;
use App\Models\Wiki\Video\VideoScript;
use App\Nova\Resources\Wiki\Video\Script;
/**
* Class VideoScriptDeleted.
*
* @extends WikiDeletedEvent<VideoScript>
*/
class VideoScriptDeleted extends WikiDeletedEvent
{
/**
* Create a new event instance.
*
* @param VideoScript $script
*/
public function __construct(VideoScript $script)
{
parent::__construct($script);
}
/**
* Get the model that has fired this event.
*
* @return VideoScript
*/
public function getModel(): VideoScript
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Script '**{$this->getModel()->getName()}**' has been deleted.";
}
/**
* Get the message for the nova notification.
*
* @return string
*/
protected function getNotificationMessage(): string
{
return "Script '**{$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 = Script::uriKey();
return "/resources/$uriKey/{$this->getModel()->getKey()}";
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Wiki\Video\Script;
use App\Events\Base\Wiki\WikiRestoredEvent;
use App\Models\Wiki\Video\VideoScript;
/**
* Class VideoScriptRestored.
*
* @extends WikiRestoredEvent<VideoScript>
*/
class VideoScriptRestored extends WikiRestoredEvent
{
/**
* Create a new event instance.
*
* @param VideoScript $script
*/
public function __construct(VideoScript $script)
{
parent::__construct($script);
}
/**
* Get the model that has fired this event.
*
* @return VideoScript
*/
public function getModel(): VideoScript
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Script '**{$this->getModel()->getName()}**' has been restored.";
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Events\Wiki\Video\Script;
use App\Events\Base\Wiki\WikiUpdatedEvent;
use App\Models\Wiki\Video\VideoScript;
/**
* Class VideoScriptUpdated.
*
* @extends WikiUpdatedEvent<VideoScript>
*/
class VideoScriptUpdated extends WikiUpdatedEvent
{
/**
* Create a new event instance.
*
* @param VideoScript $script
*/
public function __construct(VideoScript $script)
{
parent::__construct($script);
$this->initializeEmbedFields($script);
}
/**
* Get the model that has fired this event.
*
* @return VideoScript
*/
public function getModel(): VideoScript
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Script '**{$this->getModel()->getName()}**' has been updated.";
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Admin\Dump;
use App\Http\Api\Field\Field;
use App\Http\Resources\Admin\Resource\DumpResource;
/**
* Class DumpLinkField.
*/
class DumpLinkField extends Field
{
/**
* Create a new field instance.
*/
public function __construct()
{
parent::__construct(DumpResource::ATTRIBUTE_LINK);
}
}
@@ -6,9 +6,7 @@ namespace App\Http\Api\Field\Admin\Dump;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Criteria\Field\Criteria;
use App\Http\Api\Field\StringField;
use App\Http\Resources\Admin\Resource\DumpResource;
use App\Models\Admin\Dump;
use Illuminate\Http\Request;
@@ -25,18 +23,6 @@ class DumpPathField extends StringField implements CreatableField, UpdatableFiel
parent::__construct(Dump::ATTRIBUTE_PATH);
}
/**
* 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 url.
return parent::shouldSelect($criteria) || $criteria->isAllowedField(DumpResource::ATTRIBUTE_LINK);
}
/**
* Set the creation validation rules for the field.
*
@@ -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 FlagsAllowScriptDownloadingField.
*/
class FlagsAllowScriptDownloadingField extends Field
{
/**
* Create a new field instance.
*/
public function __construct()
{
parent::__construct(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG);
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Wiki\Video\Script;
use App\Http\Api\Field\Field;
use App\Http\Resources\Wiki\Video\Resource\ScriptResource;
/**
* Class ScriptLinkField.
*/
class ScriptLinkField extends Field
{
/**
* Create a new field instance.
*/
public function __construct()
{
parent::__construct(ScriptResource::ATTRIBUTE_LINK);
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Wiki\Video\Script;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Field\StringField;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Http\Request;
/**
* Class ScriptPathField.
*/
class ScriptPathField extends StringField implements CreatableField, UpdatableField
{
/**
* Create a new field instance.
*/
public function __construct()
{
parent::__construct(VideoScript::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',
];
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
'required',
'string',
'max:192',
];
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Wiki\Video\Script;
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\Video;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
/**
* Class ScriptVideoIdField.
*/
class ScriptVideoIdField extends Field implements CreatableField, SelectableField, UpdatableField
{
/**
* Create a new field instance.
*/
public function __construct()
{
parent::__construct(VideoScript::ATTRIBUTE_VIDEO);
}
/**
* 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(Video::TABLE, Video::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 video 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(Video::TABLE, Video::ATTRIBUTE_ID),
];
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Query\Wiki\Video\Script;
use App\Http\Api\Query\Base\EloquentReadQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Resources\BaseCollection;
use App\Http\Resources\BaseResource;
use App\Http\Resources\Wiki\Video\Collection\ScriptCollection;
use App\Http\Resources\Wiki\Video\Resource\ScriptResource;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Database\Eloquent\Builder;
/**
* Class ScriptReadQuery.
*/
class ScriptReadQuery extends EloquentReadQuery
{
/**
* Get the resource schema.
*
* @return EloquentSchema
*/
public function schema(): EloquentSchema
{
return new ScriptSchema();
}
/**
* Get the query builder of the resource.
*
* @return Builder
*/
public function builder(): Builder
{
return VideoScript::query();
}
/**
* Get the json resource.
*
* @param mixed $resource
* @return BaseResource
*/
public function resource(mixed $resource): BaseResource
{
return new ScriptResource($resource, $this);
}
/**
* Get the resource collection.
*
* @param mixed $resource
* @return BaseCollection
*/
public function collection(mixed $resource): BaseCollection
{
return new ScriptCollection($resource, $this);
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Query\Wiki\Video\Script;
use App\Http\Api\Query\Base\EloquentWriteQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Resources\BaseResource;
use App\Http\Resources\Wiki\Video\Resource\ScriptResource;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Database\Eloquent\Builder;
/**
* Class ScriptWriteQuery.
*/
class ScriptWriteQuery extends EloquentWriteQuery
{
/**
* Get the resource schema.
*
* @return EloquentSchema
*/
public function schema(): EloquentSchema
{
return new ScriptSchema();
}
/**
* Get the query builder of the resource.
*
* @return Builder
*/
public function builder(): Builder
{
return VideoScript::query();
}
/**
* Get the json resource.
*
* @param mixed $resource
* @return BaseResource
*/
public function resource(mixed $resource): BaseResource
{
return new ScriptResource($resource, new ScriptReadQuery());
}
}
+2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http\Api\Schema\Admin;
use App\Http\Api\Field\Admin\Dump\DumpLinkField;
use App\Http\Api\Field\Admin\Dump\DumpPathField;
use App\Http\Api\Field\Base\IdField;
use App\Http\Api\Field\Field;
@@ -59,6 +60,7 @@ class DumpSchema extends EloquentSchema
[
new IdField(Dump::ATTRIBUTE_ID),
new DumpPathField(),
new DumpLinkField(),
],
);
}
@@ -7,6 +7,7 @@ namespace App\Http\Api\Schema\Config;
use App\Http\Api\Field\Config\Flags\FlagsAllowAudioStreamsField;
use App\Http\Api\Field\Config\Flags\FlagsAllowDiscordNotificationsField;
use App\Http\Api\Field\Config\Flags\FlagsAllowDumpDownloadingField;
use App\Http\Api\Field\Config\Flags\FlagsAllowScriptDownloadingField;
use App\Http\Api\Field\Config\Flags\FlagsAllowVideoStreamsField;
use App\Http\Api\Field\Config\Flags\FlagsAllowViewRecordingField;
use App\Http\Api\Field\Field;
@@ -54,6 +55,7 @@ class FlagsSchema extends Schema
new FlagsAllowDiscordNotificationsField(),
new FlagsAllowViewRecordingField(),
new FlagsAllowDumpDownloadingField(),
new FlagsAllowScriptDownloadingField(),
];
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Schema\Wiki\Video;
use App\Http\Api\Field\Base\IdField;
use App\Http\Api\Field\Field;
use App\Http\Api\Field\Wiki\Video\Script\ScriptLinkField;
use App\Http\Api\Field\Wiki\Video\Script\ScriptPathField;
use App\Http\Api\Field\Wiki\Video\Script\ScriptVideoIdField;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\VideoSchema;
use App\Http\Resources\Wiki\Video\Resource\ScriptResource;
use App\Models\Wiki\Video\VideoScript;
/**
* Class ScriptSchema.
*/
class ScriptSchema extends EloquentSchema
{
/**
* The model this schema represents.
*
* @return string
*/
public function model(): string
{
return VideoScript::class;
}
/**
* Get the type of the resource.
*
* @return string
*/
public function type(): string
{
return ScriptResource::$wrap;
}
/**
* Get the allowed includes.
*
* @return AllowedInclude[]
*/
public function allowedIncludes(): array
{
return [
new AllowedInclude(new VideoSchema(), VideoScript::RELATION_VIDEO),
];
}
/**
* Get the direct fields of the resource.
*
* @return Field[]
*/
public function fields(): array
{
return array_merge(
parent::fields(),
[
new IdField(VideoScript::ATTRIBUTE_ID),
new ScriptPathField(),
new ScriptLinkField(),
new ScriptVideoIdField(),
],
);
}
}
+2
View File
@@ -25,6 +25,7 @@ use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Anime\Theme\EntrySchema;
use App\Http\Api\Schema\Wiki\Anime\ThemeSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Resources\Wiki\Resource\VideoResource;
use App\Models\Wiki\Video;
@@ -64,6 +65,7 @@ class VideoSchema extends EloquentSchema
new AllowedInclude(new AnimeSchema(), Video::RELATION_ANIME),
new AllowedInclude(new AudioSchema(), Video::RELATION_AUDIO),
new AllowedInclude(new EntrySchema(), Video::RELATION_ANIMETHEMEENTRIES),
new AllowedInclude(new ScriptSchema(), Video::RELATION_SCRIPT),
new AllowedInclude(new ThemeSchema(), Video::RELATION_ANIMETHEME),
];
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Wiki\Video;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Wiki\Video\Script\ScriptDestroyRequest;
use App\Http\Requests\Api\Wiki\Video\Script\ScriptForceDeleteRequest;
use App\Http\Requests\Api\Wiki\Video\Script\ScriptIndexRequest;
use App\Http\Requests\Api\Wiki\Video\Script\ScriptRestoreRequest;
use App\Http\Requests\Api\Wiki\Video\Script\ScriptShowRequest;
use App\Http\Requests\Api\Wiki\Video\Script\ScriptStoreRequest;
use App\Http\Requests\Api\Wiki\Video\Script\ScriptUpdateRequest;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Http\JsonResponse;
use Spatie\RouteDiscovery\Attributes\Route;
/**
* Class ScriptController.
*/
class ScriptController extends Controller
{
/**
* Display a listing of the resource.
*
* @param ScriptIndexRequest $request
* @return JsonResponse
*/
#[Route(fullUri: 'videoscript', name: 'videoscript.index')]
public function index(ScriptIndexRequest $request): JsonResponse
{
$query = $request->getQuery();
return $query->index()->toResponse($request);
}
/**
* Store a newly created resource.
*
* @param ScriptStoreRequest $request
* @return JsonResponse
*/
#[Route(fullUri: 'videoscript', name: 'videoscript.store', middleware: 'auth:sanctum')]
public function store(ScriptStoreRequest $request): JsonResponse
{
$resource = $request->getQuery()->store();
return $resource->toResponse($request);
}
/**
* Display the specified resource.
*
* @param ScriptShowRequest $request
* @param VideoScript $script
* @return JsonResponse
*/
#[Route(fullUri: 'videoscript/{videoscript}', name: 'videoscript.show')]
public function show(ScriptShowRequest $request, VideoScript $script): JsonResponse
{
$resource = $request->getQuery()->show($script);
return $resource->toResponse($request);
}
/**
* Update the specified resource.
*
* @param ScriptUpdateRequest $request
* @param VideoScript $script
* @return JsonResponse
*/
#[Route(fullUri: 'videoscript/{videoscript}', name: 'videoscript.update', middleware: 'auth:sanctum')]
public function update(ScriptUpdateRequest $request, VideoScript $script): JsonResponse
{
$resource = $request->getQuery()->update($script);
return $resource->toResponse($request);
}
/**
* Remove the specified resource.
*
* @param ScriptDestroyRequest $request
* @param VideoScript $script
* @return JsonResponse
*/
#[Route(fullUri: 'videoscript/{videoscript}', name: 'videoscript.destroy', middleware: 'auth:sanctum')]
public function destroy(ScriptDestroyRequest $request, VideoScript $script): JsonResponse
{
$resource = $request->getQuery()->destroy($script);
return $resource->toResponse($request);
}
/**
* Restore the specified resource.
*
* @param ScriptRestoreRequest $request
* @param VideoScript $script
* @return JsonResponse
*/
#[Route(method: 'patch', fullUri: 'restore/videoscript/{videoscript}', name: 'videoscript.restore', middleware: 'auth:sanctum')]
public function restore(ScriptRestoreRequest $request, VideoScript $script): JsonResponse
{
$resource = $request->getQuery()->restore($script);
return $resource->toResponse($request);
}
/**
* Hard-delete the specified resource.
*
* @param ScriptForceDeleteRequest $request
* @param VideoScript $script
* @return JsonResponse
*/
#[Route(method: 'delete', fullUri: 'forceDelete/videoscript/{videoscript}', name: 'videoscript.forceDelete', middleware: 'auth:sanctum')]
public function forceDelete(ScriptForceDeleteRequest $request, VideoScript $script): JsonResponse
{
return $request->getQuery()->forceDelete($script);
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Wiki\Video\Script;
use App\Constants\Config\VideoConstants;
use App\Http\Controllers\Controller;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* Class ScriptController.
*/
class ScriptController extends Controller
{
/**
* Download dump.
*
* @param VideoScript $script
* @return StreamedResponse
*/
public function show(VideoScript $script): StreamedResponse
{
/** @var FilesystemAdapter $fs */
$fs = Storage::disk(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
return $fs->download($script->path);
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\Wiki\Video\Script;
use App\Http\Api\Query\Base\EloquentWriteQuery;
use App\Http\Api\Query\Wiki\Video\Script\ScriptWriteQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Requests\Api\Base\EloquentDestroyRequest;
/**
* Class ScriptDestroyRequest.
*/
class ScriptDestroyRequest extends EloquentDestroyRequest
{
/**
* Get the schema.
*
* @return EloquentSchema
*/
protected function schema(): EloquentSchema
{
return new ScriptSchema();
}
/**
* Get the validation API Query.
*
* @return EloquentWriteQuery
*/
public function getQuery(): EloquentWriteQuery
{
return new ScriptWriteQuery($this->validated());
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\Wiki\Video\Script;
use App\Http\Api\Query\Base\EloquentWriteQuery;
use App\Http\Api\Query\Wiki\Video\Script\ScriptWriteQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Requests\Api\Base\EloquentForceDeleteRequest;
/**
* Class ScriptForceDeleteRequest.
*/
class ScriptForceDeleteRequest extends EloquentForceDeleteRequest
{
/**
* Get the schema.
*
* @return EloquentSchema
*/
protected function schema(): EloquentSchema
{
return new ScriptSchema();
}
/**
* Get the validation API Query.
*
* @return EloquentWriteQuery
*/
public function getQuery(): EloquentWriteQuery
{
return new ScriptWriteQuery($this->validated());
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\Wiki\Video\Script;
use App\Http\Api\Query\Base\EloquentReadQuery;
use App\Http\Api\Query\Wiki\Video\Script\ScriptReadQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Requests\Api\Base\EloquentIndexRequest;
/**
* Class ScriptIndexRequest.
*/
class ScriptIndexRequest extends EloquentIndexRequest
{
/**
* Get the schema.
*
* @return EloquentSchema
*/
protected function schema(): EloquentSchema
{
return new ScriptSchema();
}
/**
* Get the validation API Query.
*
* @return EloquentReadQuery
*/
public function getQuery(): EloquentReadQuery
{
return new ScriptReadQuery($this->validated());
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\Wiki\Video\Script;
use App\Http\Api\Query\Base\EloquentWriteQuery;
use App\Http\Api\Query\Wiki\Video\Script\ScriptWriteQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Requests\Api\Base\EloquentRestoreRequest;
/**
* Class ScriptRestoreRequest.
*/
class ScriptRestoreRequest extends EloquentRestoreRequest
{
/**
* Get the schema.
*
* @return EloquentSchema
*/
protected function schema(): EloquentSchema
{
return new ScriptSchema();
}
/**
* Get the validation API Query.
*
* @return EloquentWriteQuery
*/
public function getQuery(): EloquentWriteQuery
{
return new ScriptWriteQuery($this->validated());
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\Wiki\Video\Script;
use App\Http\Api\Query\Base\EloquentReadQuery;
use App\Http\Api\Query\Wiki\Video\Script\ScriptReadQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Requests\Api\Base\EloquentShowRequest;
/**
* Class ScriptShowRequest.
*/
class ScriptShowRequest extends EloquentShowRequest
{
/**
* Get the schema.
*
* @return EloquentSchema
*/
protected function schema(): EloquentSchema
{
return new ScriptSchema();
}
/**
* Get the validation API Query.
*
* @return EloquentReadQuery
*/
public function getQuery(): EloquentReadQuery
{
return new ScriptReadQuery($this->validated());
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\Wiki\Video\Script;
use App\Http\Api\Query\Base\EloquentWriteQuery;
use App\Http\Api\Query\Wiki\Video\Script\ScriptWriteQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Requests\Api\Base\EloquentStoreRequest;
/**
* Class ScriptStoreRequest.
*/
class ScriptStoreRequest extends EloquentStoreRequest
{
/**
* Get the schema.
*
* @return EloquentSchema
*/
protected function schema(): EloquentSchema
{
return new ScriptSchema();
}
/**
* Get the validation API Query.
*
* @return EloquentWriteQuery
*/
public function getQuery(): EloquentWriteQuery
{
return new ScriptWriteQuery($this->validated());
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\Wiki\Video\Script;
use App\Http\Api\Query\Base\EloquentWriteQuery;
use App\Http\Api\Query\Wiki\Video\Script\ScriptWriteQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Requests\Api\Base\EloquentUpdateRequest;
/**
* Class ScriptUpdateRequest.
*/
class ScriptUpdateRequest extends EloquentUpdateRequest
{
/**
* Get the schema.
*
* @return EloquentSchema
*/
protected function schema(): EloquentSchema
{
return new ScriptSchema();
}
/**
* Get the validation API Query.
*
* @return EloquentWriteQuery
*/
public function getQuery(): EloquentWriteQuery
{
return new ScriptWriteQuery($this->validated());
}
}
@@ -66,6 +66,10 @@ class FlagsResource extends BaseResource
$result[FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG] = Config::bool(FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED);
}
if ($this->isAllowedField(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG)) {
$result[FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG] = Config::bool(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED);
}
return $result;
}
}
@@ -7,6 +7,7 @@ namespace App\Http\Resources\Wiki\Resource;
use App\Http\Api\Query\ReadQuery;
use App\Http\Resources\BaseResource;
use App\Http\Resources\Wiki\Anime\Theme\Collection\EntryCollection;
use App\Http\Resources\Wiki\Video\Resource\ScriptResource;
use App\Models\BaseModel;
use App\Models\Wiki\Video;
use Illuminate\Http\Request;
@@ -126,6 +127,7 @@ class VideoResource extends BaseResource
$result[Video::RELATION_ANIMETHEMEENTRIES] = new EntryCollection($this->whenLoaded(Video::RELATION_ANIMETHEMEENTRIES), $this->query);
$result[Video::RELATION_AUDIO] = new AudioResource($this->whenLoaded(Video::RELATION_AUDIO), $this->query);
$result[Video::RELATION_SCRIPT] = new ScriptResource($this->whenLoaded(Video::RELATION_SCRIPT), $this->query);
return $result;
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Wiki\Video\Collection;
use App\Http\Resources\BaseCollection;
use App\Http\Resources\Wiki\Video\Resource\ScriptResource;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Http\Request;
/**
* Class ScriptCollection.
*/
class ScriptCollection extends BaseCollection
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'videoscripts';
/**
* Transform the resource into a JSON array.
*
* @param Request $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toArray($request): array
{
return $this->collection->map(fn (VideoScript $script) => new ScriptResource($script, $this->query))->all();
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Wiki\Video\Resource;
use App\Http\Api\Query\ReadQuery;
use App\Http\Resources\BaseResource;
use App\Http\Resources\Wiki\Resource\VideoResource;
use App\Models\BaseModel;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\MissingValue;
/**
* Class ScriptResource.
*
* @mixin VideoScript
*/
class ScriptResource extends BaseResource
{
final public const ATTRIBUTE_LINK = 'link';
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'videoscript';
/**
* Create a new resource instance.
*
* @param VideoScript | MissingValue | null $script
* @param ReadQuery $query
* @return void
*/
public function __construct(VideoScript|MissingValue|null $script, ReadQuery $query)
{
parent::__construct($script, $query);
}
/**
* Transform the resource into an array.
*
* @param Request $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toArray($request): array
{
$result = [];
if ($this->isAllowedField(BaseResource::ATTRIBUTE_ID)) {
$result[BaseResource::ATTRIBUTE_ID] = $this->getKey();
}
if ($this->isAllowedField(VideoScript::ATTRIBUTE_PATH)) {
$result[VideoScript::ATTRIBUTE_PATH] = $this->path;
}
if ($this->isAllowedField(BaseModel::ATTRIBUTE_CREATED_AT)) {
$result[BaseModel::ATTRIBUTE_CREATED_AT] = $this->created_at;
}
if ($this->isAllowedField(BaseModel::ATTRIBUTE_UPDATED_AT)) {
$result[BaseModel::ATTRIBUTE_UPDATED_AT] = $this->updated_at;
}
if ($this->isAllowedField(BaseModel::ATTRIBUTE_DELETED_AT)) {
$result[BaseModel::ATTRIBUTE_DELETED_AT] = $this->deleted_at;
}
if ($this->isAllowedField(ScriptResource::ATTRIBUTE_LINK)) {
$result[ScriptResource::ATTRIBUTE_LINK] = route('videoscript.show', $this);
}
$result[VideoScript::RELATION_VIDEO] = new VideoResource($this->whenLoaded(VideoScript::RELATION_VIDEO), $this->query);
return $result;
}
}
+14
View File
@@ -14,6 +14,7 @@ use App\Events\Wiki\Video\VideoRestored;
use App\Events\Wiki\Video\VideoUpdated;
use App\Models\BaseModel;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video\VideoScript;
use App\Pivots\AnimeThemeEntryVideo;
use BenSampo\Enum\Enum;
use CyrildeWit\EloquentViewable\Contracts\Viewable;
@@ -23,6 +24,7 @@ use Elastic\ScoutDriverPlus\Searchable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection;
use Laravel\Nova\Actions\Actionable;
@@ -40,6 +42,7 @@ use Laravel\Nova\Actions\Actionable;
* @property Enum $overlap
* @property string $path
* @property int|null $resolution
* @property VideoScript|null $script
* @property int $size
* @property Enum|null $source
* @property bool $subbed
@@ -78,6 +81,7 @@ class Video extends BaseModel implements Streamable, Viewable
final public const RELATION_ANIMETHEME = 'animethemeentries.animetheme';
final public const RELATION_ANIMETHEMEENTRIES = 'animethemeentries';
final public const RELATION_AUDIO = 'audio';
final public const RELATION_SCRIPT = 'videoscript';
final public const RELATION_SONG = 'animethemeentries.animetheme.song';
/**
@@ -323,4 +327,14 @@ class Video extends BaseModel implements Streamable, Viewable
{
return $this->belongsTo(Audio::class, Video::ATTRIBUTE_AUDIO);
}
/**
* Get the script that the video owns.
*
* @return HasOne
*/
public function videoscript(): HasOne
{
return $this->hasOne(VideoScript::class, VideoScript::ATTRIBUTE_VIDEO);
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Models\Wiki\Video;
use App\Events\Wiki\Video\Script\VideoScriptCreated;
use App\Events\Wiki\Video\Script\VideoScriptDeleted;
use App\Events\Wiki\Video\Script\VideoScriptRestored;
use App\Events\Wiki\Video\Script\VideoScriptUpdated;
use App\Models\BaseModel;
use App\Models\Wiki\Video;
use Database\Factories\Wiki\Video\VideoScriptFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Nova\Actions\Actionable;
/**
* Class VideoScript.
*
* @property int $script_id
* @property string $path
* @property Video $video
* @property int $video_id
*
* @method static VideoScriptFactory factory(...$parameters)
*/
class VideoScript extends BaseModel
{
use Actionable;
final public const TABLE = 'video_scripts';
final public const ATTRIBUTE_ID = 'script_id';
final public const ATTRIBUTE_PATH = 'path';
final public const ATTRIBUTE_VIDEO = 'video_id';
final public const RELATION_VIDEO = 'video';
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
VideoScript::ATTRIBUTE_PATH,
];
/**
* The event map for the model.
*
* Allows for object-based events for native Eloquent events.
*
* @var array
*/
protected $dispatchesEvents = [
'created' => VideoScriptCreated::class,
'deleted' => VideoScriptDeleted::class,
'restored' => VideoScriptRestored::class,
'updated' => VideoScriptUpdated::class,
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = VideoScript::TABLE;
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = VideoScript::ATTRIBUTE_ID;
/**
* Get name.
*
* @return string
*/
public function getName(): string
{
return $this->path;
}
/**
* Get the video that owns the script.
*
* @return BelongsTo
*/
public function video(): BelongsTo
{
return $this->belongsTo(Video::class, VideoScript::ATTRIBUTE_VIDEO);
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Nova\Actions\Repositories\Storage\Wiki\Video\Script;
use App\Concerns\Repositories\Wiki\Video\ReconcilesScriptRepositories;
use App\Constants\Config\VideoConstants;
use App\Nova\Actions\Repositories\Storage\ReconcileStorageAction;
use Illuminate\Support\Facades\Config;
/**
* Class ReconcileScriptAction.
*/
class ReconcileScriptAction extends ReconcileStorageAction
{
use ReconcilesScriptRepositories;
/**
* Get the displayable name of the action.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function name(): string
{
return __('nova.actions.repositories.name', ['label' => __('nova.resources.label.video_scripts')]);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
}
}
+1 -2
View File
@@ -9,7 +9,6 @@ use App\Contracts\Storage\InteractsWithDisk;
use App\Nova\Actions\Storage\StorageAction;
use App\Rules\Storage\StorageFileDirectoryExistsRule;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Fields\Text;
@@ -32,7 +31,7 @@ abstract class MoveAction extends StorageAction implements InteractsWithDisk
{
$defaultPath = $this->defaultPath($request);
$fs = Storage::disk(Config::get($this->disk()));
$fs = Storage::disk($this->disk());
return [
Text::make(__('nova.actions.storage.move.fields.path.name'), 'path')
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Nova\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\DeleteScriptAction as DeleteScript;
use App\Models\Wiki\Video\VideoScript;
use App\Nova\Actions\Storage\Base\DeleteAction;
use Illuminate\Support\Collection;
use Laravel\Nova\Fields\ActionFields;
/**
* Class DeleteScriptAction.
*/
class DeleteScriptAction extends DeleteAction
{
/**
* Get the displayable name of the action.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function name(): string
{
return __('nova.actions.video_script.delete.name');
}
/**
* Get the underlying storage action.
*
* @param ActionFields $fields
* @param Collection<int, VideoScript> $models
* @return DeleteScript
*/
protected function action(ActionFields $fields, Collection $models): DeleteScript
{
$script = $models->first();
return new DeleteScript($script);
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Nova\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\MoveScriptAction as MoveScript;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use App\Nova\Actions\Storage\Base\MoveAction;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Http\Requests\NovaRequest;
/**
* Class MoveScriptAction.
*/
class MoveScriptAction extends MoveAction
{
/**
* Get the displayable name of the action.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function name(): string
{
return __('nova.actions.video_script.move.name');
}
/**
* Get the underlying storage action.
*
* @param ActionFields $fields
* @param Collection<int, VideoScript> $models
* @return MoveScript
*/
protected function action(ActionFields $fields, Collection $models): MoveScript
{
$path = $fields->get('path');
$script = $models->first();
return new MoveScript($script, $path);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
}
/**
* Resolve the default value for the path field.
*
* @param NovaRequest $request
* @return string|null
*/
protected function defaultPath(NovaRequest $request): ?string
{
$script = $request->findModelQuery()->first();
return $script instanceof VideoScript
? $script->path
: null;
}
/**
* The file extension that the path must end with.
*
* @return string
*/
protected function allowedFileExtension(): string
{
return '.txt';
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Nova\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\UploadScriptAction as UploadScript;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video;
use App\Nova\Actions\Storage\Base\UploadAction;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Validation\Rules\File as FileRule;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Http\Requests\NovaRequest;
/**
* Class UploadScriptAction.
*/
class UploadScriptAction extends UploadAction
{
/**
* Get the displayable name of the action.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function name(): string
{
return __('nova.actions.video_script.upload.name');
}
/**
* Get the fields available on the action.
*
* @param NovaRequest $request
* @return array
*/
public function fields(NovaRequest $request): array
{
$model = $request->findModelQuery()->first();
return array_merge(
parent::fields($request),
[
Hidden::make(__('nova.resources.singularLabel.video'), Video::ATTRIBUTE_ID)
->default(fn () => $model instanceof Video ? $model->getKey() : null),
],
);
}
/**
* Get the underlying storage action.
*
* @param ActionFields $fields
* @param Collection $models
* @return UploadScript
*/
protected function action(ActionFields $fields, Collection $models): UploadScript
{
/** @var UploadedFile $file */
$file = $fields->get('file');
$path = $fields->get('path');
/** @var Video|null $video */
$video = Video::query()->find($fields->get(Video::ATTRIBUTE_ID));
return new UploadScript($file, $path, $video);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
}
/**
* Get the file validation rules.
*
* @return array
*/
protected function fileRules(): array
{
return [
'required',
FileRule::types('txt')->max(2 * 1024),
];
}
}
@@ -33,6 +33,8 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Validation\Rules\File as FileRule;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Fields\File;
use Laravel\Nova\Fields\Heading;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Http\Requests\NovaRequest;
@@ -64,10 +66,20 @@ class UploadVideoAction extends UploadAction
$parent = $request->findParentModel();
return array_merge(
[
Heading::make(__('nova.resources.singularLabel.video')),
],
parent::fields($request),
[
Hidden::make(__('nova.resources.singularLabel.anime_theme_entry'), AnimeThemeEntry::ATTRIBUTE_ID)
->default(fn () => $parent instanceof AnimeThemeEntry ? $parent->getKey() : null),
Heading::make(__('nova.resources.singularLabel.video_script')),
File::make(__('nova.resources.singularLabel.video_script'), 'script')
->nullable()
->rules(['nullable', FileRule::types('txt')->max(2 * 1024)])
->help(__('nova.actions.storage.upload.fields.file.help')),
],
);
}
@@ -81,14 +93,18 @@ class UploadVideoAction extends UploadAction
*/
protected function action(ActionFields $fields, Collection $models): UploadVideo
{
$path = $fields->get('path');
/** @var UploadedFile $file */
$file = $fields->get('file');
$path = $fields->get('path');
/** @var AnimeThemeEntry|null $entry */
$entry = AnimeThemeEntry::query()->find($fields->get(AnimeThemeEntry::ATTRIBUTE_ID));
return new UploadVideo($file, $path, $entry);
/** @var UploadedFile|null $script */
$script = $fields->get('script');
return new UploadVideo($file, $path, $entry, $script);
}
/**
+144
View File
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Nova\Lenses\Video;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use App\Enums\Models\Wiki\VideoOverlap;
use App\Enums\Models\Wiki\VideoSource;
use App\Models\BaseModel;
use App\Models\Wiki\Video;
use App\Nova\Lenses\BaseLens;
use BenSampo\Enum\Enum;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
/**
* Class VideoAudioLens.
*/
class VideoScriptLens extends BaseLens
{
/**
* Get the displayable name of the lens.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function name(): string
{
return __('nova.lenses.video.script.name');
}
/**
* The criteria used to refine the models for the lens.
*
* @param Builder $query
* @return Builder
*/
public static function criteria(Builder $query): Builder
{
return $query->whereDoesntHave(Video::RELATION_SCRIPT)
->where(Video::ATTRIBUTE_PATH, ComparisonOperator::NOTLIKE, 'misc%');
}
/**
* Get the fields available to the lens.
*
* @param NovaRequest $request
* @return array
*
* @throws Exception
*/
public function fields(NovaRequest $request): array
{
return [
ID::make(__('nova.fields.base.id'), Video::ATTRIBUTE_ID)
->sortable()
->showOnPreview(),
Text::make(__('nova.fields.video.filename.name'), Video::ATTRIBUTE_FILENAME)
->sortable()
->copyable()
->showOnPreview()
->filterable(),
Number::make(__('nova.fields.video.resolution.name'), Video::ATTRIBUTE_RESOLUTION)
->sortable()
->showOnPreview()
->filterable(),
Boolean::make(__('nova.fields.video.nc.name'), Video::ATTRIBUTE_NC)
->sortable()
->showOnPreview()
->filterable(),
Boolean::make(__('nova.fields.video.subbed.name'), Video::ATTRIBUTE_SUBBED)
->sortable()
->showOnPreview()
->filterable(),
Boolean::make(__('nova.fields.video.lyrics.name'), Video::ATTRIBUTE_LYRICS)
->sortable()
->showOnPreview()
->filterable(),
Boolean::make(__('nova.fields.video.uncen.name'), Video::ATTRIBUTE_UNCEN)
->sortable()
->showOnPreview()
->filterable(),
Select::make(__('nova.fields.video.overlap.name'), Video::ATTRIBUTE_OVERLAP)
->options(VideoOverlap::asSelectArray())
->displayUsing(fn (?Enum $enum) => $enum?->description)
->onlyOnPreview(),
Select::make(__('nova.fields.video.source.name'), Video::ATTRIBUTE_SOURCE)
->options(VideoSource::asSelectArray())
->displayUsing(fn (?Enum $enum) => $enum?->description)
->onlyOnPreview(),
DateTime::make(__('nova.fields.base.created_at'), BaseModel::ATTRIBUTE_CREATED_AT)
->onlyOnPreview(),
DateTime::make(__('nova.fields.base.updated_at'), BaseModel::ATTRIBUTE_UPDATED_AT)
->onlyOnPreview(),
DateTime::make(__('nova.fields.base.deleted_at'), BaseModel::ATTRIBUTE_DELETED_AT)
->onlyOnPreview(),
];
}
/**
* Get the actions available on the lens.
*
* @param NovaRequest $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function actions(NovaRequest $request): array
{
return [];
}
/**
* Get the URI key for the lens.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function uriKey(): string
{
return 'video-script-lens';
}
}
+22 -1
View File
@@ -12,15 +12,18 @@ use App\Nova\Actions\Models\Wiki\Video\BackfillAudioAction;
use App\Nova\Actions\Repositories\Storage\Wiki\Video\ReconcileVideoAction;
use App\Nova\Actions\Storage\Wiki\Video\DeleteVideoAction;
use App\Nova\Actions\Storage\Wiki\Video\MoveVideoAction;
use App\Nova\Actions\Storage\Wiki\Video\Script\UploadScriptAction;
use App\Nova\Actions\Storage\Wiki\Video\UploadVideoAction;
use App\Nova\Lenses\Video\VideoAudioLens;
use App\Nova\Lenses\Video\VideoResolutionLens;
use App\Nova\Lenses\Video\VideoScriptLens;
use App\Nova\Lenses\Video\VideoSourceLens;
use App\Nova\Lenses\Video\VideoUnlinkedLens;
use App\Nova\Metrics\Video\NewVideos;
use App\Nova\Metrics\Video\VideosPerDay;
use App\Nova\Resources\BaseResource;
use App\Nova\Resources\Wiki\Anime\Theme\Entry;
use App\Nova\Resources\Wiki\Video\Script;
use App\Pivots\BasePivot;
use BenSampo\Enum\Enum;
use BenSampo\Enum\Rules\EnumValue;
@@ -31,6 +34,7 @@ use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\BelongsToMany;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\HasOne;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Select;
@@ -205,6 +209,12 @@ class Video extends BaseResource
->hideWhenCreating(),
]),
HasOne::make(__('nova.resources.singularLabel.video_script'), VideoModel::RELATION_SCRIPT, Script::class)
->hideFromIndex()
->sortable()
->nullable()
->showOnPreview(),
Panel::make(__('nova.fields.base.file_properties'), $this->fileProperties())
->collapsable(),
@@ -327,6 +337,16 @@ class Video extends BaseResource
return $user instanceof User && $user->can('create video');
}),
(new UploadScriptAction())
->confirmButtonText(__('nova.actions.storage.upload.confirmButtonText'))
->cancelButtonText(__('nova.actions.base.cancelButtonText'))
->onlyOnDetail()
->canSee(function (Request $request) {
$user = $request->user();
return $user instanceof User && $user->can('create video script');
}),
]
);
}
@@ -363,7 +383,8 @@ class Video extends BaseResource
new VideoResolutionLens(),
new VideoSourceLens(),
new VideoUnlinkedLens(),
]
new VideoScriptLens(),
],
);
}
}
+210
View File
@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace App\Nova\Resources\Wiki\Video;
use App\Models\Auth\User;
use App\Models\Wiki\Video\VideoScript;
use App\Nova\Actions\Repositories\Storage\Wiki\Video\Script\ReconcileScriptAction;
use App\Nova\Actions\Storage\Wiki\Video\Script\DeleteScriptAction;
use App\Nova\Actions\Storage\Wiki\Video\Script\MoveScriptAction;
use App\Nova\Actions\Storage\Wiki\Video\Script\UploadScriptAction;
use App\Nova\Resources\BaseResource;
use App\Nova\Resources\Wiki\Video;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Panel;
use Laravel\Nova\Query\Search\Column;
/**
* Class Script.
*/
class Script extends BaseResource
{
/**
* The model the resource corresponds to.
*
* @var string
*/
public static string $model = VideoScript::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = VideoScript::ATTRIBUTE_PATH;
/**
* The logical group associated with the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function group(): string
{
return __('nova.resources.group.wiki');
}
/**
* Get the displayable label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function label(): string
{
return __('nova.resources.label.video_scripts');
}
/**
* Get the displayable singular label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function singularLabel(): string
{
return __('nova.resources.singularLabel.video_script');
}
/**
* Get the URI key for the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function uriKey(): string
{
return 'video-scripts';
}
/**
* Get the searchable columns for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function searchableColumns(): array
{
return [
new Column(VideoScript::ATTRIBUTE_PATH),
];
}
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* @param NovaRequest $request
* @param Builder $query
* @return Builder
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function indexQuery(NovaRequest $request, $query): Builder
{
return $query->with(VideoScript::RELATION_VIDEO);
}
/**
* Get the fields displayed by the resource.
*
* @param NovaRequest $request
* @return array
*
* @throws Exception
*/
public function fields(NovaRequest $request): array
{
return [
BelongsTo::make(__('nova.resources.singularLabel.video'), VideoScript::RELATION_VIDEO, Video::class)
->sortable()
->filterable()
->searchable()
->withSubtitles()
->nullable()
->showOnPreview(),
ID::make(__('nova.fields.base.id'), VideoScript::ATTRIBUTE_ID)
->sortable()
->showOnPreview(),
Text::make(__('nova.fields.video_script.path'), VideoScript::ATTRIBUTE_PATH)
->copyable()
->showOnPreview()
->filterable(),
Panel::make(__('nova.fields.base.timestamps'), $this->timestamps()),
];
}
/**
* Get the actions available for the resource.
*
* @param NovaRequest $request
* @return array
*/
public function actions(NovaRequest $request): array
{
return array_merge(
parent::actions($request),
[
(new UploadScriptAction())
->confirmButtonText(__('nova.actions.storage.upload.confirmButtonText'))
->cancelButtonText(__('nova.actions.base.cancelButtonText'))
->onlyOnIndex()
->standalone()
->canSee(function (Request $request) {
$user = $request->user();
return $user instanceof User && $user->can('create video script');
}),
(new MoveScriptAction())
->confirmButtonText(__('nova.actions.storage.move.confirmButtonText'))
->cancelButtonText(__('nova.actions.base.cancelButtonText'))
->exceptOnIndex()
->canSee(function (Request $request) {
$user = $request->user();
return $user instanceof User && $user->can('create video script');
}),
(new DeleteScriptAction())
->confirmText(__('nova.actions.video_script.delete.confirmText'))
->confirmButtonText(__('nova.actions.storage.delete.confirmButtonText'))
->cancelButtonText(__('nova.actions.base.cancelButtonText'))
->exceptOnIndex()
->canSee(function (Request $request) {
$user = $request->user();
return $user instanceof User && $user->can('delete video script');
}),
(new ReconcileScriptAction())
->confirmButtonText(__('nova.actions.repositories.confirmButtonText'))
->cancelButtonText(__('nova.actions.base.cancelButtonText'))
->onlyOnIndex()
->standalone()
->canSee(function (Request $request) {
$user = $request->user();
return $user instanceof User && $user->can('create video script');
}),
],
);
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Policies\Wiki\Video;
use App\Models\Auth\User;
use Illuminate\Auth\Access\HandlesAuthorization;
/**
* Class VideoScriptPolicy.
*/
class VideoScriptPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @param User $user
* @return bool
*/
public function viewAny(User $user): bool
{
return $user->can('view video script');
}
/**
* Determine whether the user can view the model.
*
* @param User $user
* @return bool
*/
public function view(User $user): bool
{
return $user->can('view video script');
}
/**
* Determine whether the user can create models.
*
* @param User $user
* @return bool
*/
public function create(User $user): bool
{
return $user->can('create video script');
}
/**
* Determine whether the user can update the model.
*
* @param User $user
* @return bool
*/
public function update(User $user): bool
{
return $user->can('update video script');
}
/**
* Determine whether the user can delete the model.
*
* @param User $user
* @return bool
*/
public function delete(User $user): bool
{
return $user->can('delete video script');
}
/**
* Determine whether the user can restore the model.
*
* @param User $user
* @return bool
*/
public function restore(User $user): bool
{
return $user->can('restore video script');
}
/**
* Determine whether the user can permanently delete the model.
*
* @param User $user
* @return bool
*/
public function forceDelete(User $user): bool
{
return $user->can('force delete video script');
}
}
+7
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Providers;
use App\Constants\Config\AudioConstants;
use App\Constants\Config\DumpConstants;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
@@ -48,12 +49,17 @@ class AppServiceProvider extends ServiceProvider
'Streaming Method' => fn () => Config::get(AudioConstants::STREAMING_METHOD_QUALIFIED),
]);
AboutCommand::add('Dumps', [
'Disk' => fn () => Config::get(DumpConstants::DISK_QUALIFIED),
]);
AboutCommand::add('Flags', [
'Allow Audio Streams' => fn () => Config::bool(FlagConstants::ALLOW_AUDIO_STREAMS_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow Discord Notifications' => fn () => Config::bool(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow Video Streams' => fn () => Config::bool(FlagConstants::ALLOW_VIDEO_STREAMS_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow View Recording' => fn () => Config::bool(FlagConstants::ALLOW_VIEW_RECORDING_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow Dump Downloading' => fn () => Config::bool(FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow Script Downloading' => fn () => Config::bool(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED) ? 'true' : 'false',
]);
AboutCommand::add('Images', [
@@ -65,6 +71,7 @@ class AppServiceProvider extends ServiceProvider
'Disks' => fn () => implode(',', Config::get(VideoConstants::DISKS_QUALIFIED)),
'Encoder Version' => fn () => Config::get(VideoConstants::ENCODER_VERSION_QUALIFIED),
'Nginx Redirect' => fn () => Config::get(VideoConstants::NGINX_REDIRECT_QUALIFIED),
'Script Disk' => fn () => Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED),
'Streaming Method' => fn () => Config::get(VideoConstants::STREAMING_METHOD_QUALIFIED),
]);
+9
View File
@@ -11,6 +11,7 @@ use App\Models\Auth\User;
use App\Models\Wiki\Anime\AnimeSynonym;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
@@ -51,6 +52,9 @@ class RouteServiceProvider extends ServiceProvider
// Anime Theme Resources
Route::model('animethemeentry', AnimeThemeEntry::class);
// Video Resources
Route::model('videoscript', VideoScript::class);
$this->routes(function () {
Route::middleware('web')
->domain(Config::get('web.url'))
@@ -72,6 +76,11 @@ class RouteServiceProvider extends ServiceProvider
->prefix(Config::get(DumpConstants::PATH_QUALIFIED))
->group(base_path('routes/dump.php'));
Route::middleware('web')
->domain(Config::get(VideoConstants::SCRIPT_URL_QUALIFIED))
->prefix(Config::get(VideoConstants::SCRIPT_PATH_QUALIFIED))
->group(base_path('routes/script.php'));
Route::middleware('api')
->domain(Config::get('api.url'))
->prefix(Config::get('api.path'))
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Repositories\Eloquent\Wiki\Video;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use App\Models\Wiki\Video\VideoScript;
use App\Repositories\Eloquent\EloquentRepository;
use Illuminate\Database\Eloquent\Builder;
/**
* Class ScriptRepository.
*
* @extends EloquentRepository<VideoScript>
*/
class ScriptRepository extends EloquentRepository
{
/**
* Get the underlying query builder.
*
* @return Builder
*/
protected function builder(): Builder
{
return VideoScript::query();
}
/**
* Filter repository models.
*
* @param string $filter
* @param mixed $value
* @return void
*/
public function handleFilter(string $filter, mixed $value = null): void
{
if ($filter === 'path') {
$this->query->where(VideoScript::ATTRIBUTE_PATH, ComparisonOperator::LIKE, "$value%");
}
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Repositories\Storage\Wiki\Video;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use App\Repositories\Storage\StorageRepository;
use Closure;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use League\Flysystem\StorageAttributes;
/**
* Class ScriptRepository.
*
* @extends StorageRepository<VideoScript>
*/
class ScriptRepository extends StorageRepository
{
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
}
/**
* Return the callback to filter filesystem contents.
*
* @return Closure(StorageAttributes): bool
*/
protected function filterCallback(): Closure
{
return fn (StorageAttributes $file) => $file->isFile() && File::extension($file->path()) === 'txt';
}
/**
* Map filesystem files to model.
*
* @return Closure(StorageAttributes): VideoScript
*/
protected function mapCallback(): Closure
{
return fn (StorageAttributes $file) => new VideoScript([
VideoScript::ATTRIBUTE_PATH => $file->path(),
]);
}
}
+22
View File
@@ -156,6 +156,27 @@ return [
'visibility' => 'public',
'throw' => false,
],
'scripts' => [
'driver' => 's3',
'key' => env('SCRIPT_ACCESS_KEY_ID'),
'secret' => env('SCRIPT_SECRET_ACCESS_KEY'),
'region' => env('SCRIPT_DEFAULT_REGION'),
'bucket' => env('SCRIPT_BUCKET'),
'endpoint' => env('SCRIPT_ENDPOINT'),
'stream_reads' => env('SCRIPT_STREAM_READS'),
'disable_asserts' => env('SCRIPT_DISABLE_ASSERTS'),
'visibility' => env('SCRIPT_VISIBILITY'),
'throw' => false,
],
'scripts_local' => [
'driver' => 'local',
'root' => public_path('scripts'),
'url' => env('APP_URL').'/scripts',
'visibility' => 'public',
'throw' => false,
],
],
/*
@@ -175,6 +196,7 @@ return [
public_path('images') => env('IMAGE_DISK_ROOT', storage_path('app/images')),
public_path('videos') => env('VIDEO_DISK_ROOT', storage_path('app/videos')),
public_path('dumps') => env('DUMP_DISK_ROOT', storage_path('app/dumps')),
public_path('scripts') => env('SCRIPT_DISK_ROOT', storage_path('app/scripts')),
],
];
+13
View File
@@ -69,4 +69,17 @@ return [
*/
'allow_dump_downloading' => (bool) env('ALLOW_DUMP_DOWNLOADING', false),
/*
|--------------------------------------------------------------------------
| Allow Script Downloading
|--------------------------------------------------------------------------
|
| When script downloads are allowed, requests to the videoscript.show route will
| download scripts. If disabled, requests to the videoscript.show route will
| raise a 403 Forbidden response.
|
*/
'allow_script_downloading' => (bool) env('ALLOW_SCRIPT_DOWNLOADING', false),
];
+1
View File
@@ -92,6 +92,7 @@ return [
FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED => FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG,
FlagConstants::ALLOW_VIEW_RECORDING_FLAG_QUALIFIED => FlagConstants::ALLOW_VIEW_RECORDING_FLAG,
FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED => FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG,
FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED => FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG,
VideoConstants::ENCODER_VERSION_QUALIFIED => VideoConstants::ENCODER_VERSION,
WikiConstants::FEATURED_ENTRY_SETTING_QUALIFIED => WikiConstants::FEATURED_ENTRY_SETTING,
WikiConstants::FEATURED_VIDEO_SETTING_QUALIFIED => WikiConstants::FEATURED_VIDEO_SETTING,
+44
View File
@@ -64,4 +64,48 @@ return [
*/
'encoder_version' => env('VIDEO_ENCODER_VERSION'),
/*
|--------------------------------------------------------------------------
| Video Scripts
|--------------------------------------------------------------------------
|
| Scripts represent the encoding script used to produce a video.
| Generally, a script file will be a list of FFmpeg commands
| to be executed in a target runtime environment.
|
*/
'script' => [
/*
|--------------------------------------------------------------------------
| Script Disk
|--------------------------------------------------------------------------
|
| The filesystem disk where video scripts are hosted. By default, it is assumed
| that video scripts are hosted in an S3-like bucket.
|
*/
'disk' => env('SCRIPT_DISK', 'scripts'),
/*
|--------------------------------------------------------------------------
| Script Domain
|--------------------------------------------------------------------------
|
| These values represent the base URL that video scripts are downloaded from.
| It is most likely that only one of these values should be set.
| If scripts are downloaded from a subdomain, set SCRIPT_URL and leave SCRIPT_PATH null.
| Ex: script.animethemes.test
| If scripts are NOT downloaded from a subdomain, set SCRIPT_PATH and leave SCRIPT_URL null.
| Ex: animethemes.test/script
|
*/
'url' => env('SCRIPT_URL'),
'path' => env('SCRIPT_PATH'),
],
];
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Database\Factories\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* Class VideoScriptFactory.
*
* @method VideoScript createOne($attributes = [])
* @method VideoScript makeOne($attributes = [])
*
* @extends Factory<VideoScript>
*/
class VideoScriptFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<VideoScript>
*/
protected $model = VideoScript::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition(): array
{
return [
VideoScript::ATTRIBUTE_PATH => Str::random(),
];
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use App\Models\BaseModel;
use App\Models\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
if (! Schema::hasTable(VideoScript::TABLE)) {
Schema::create(VideoScript::TABLE, function (Blueprint $table) {
$table->id(VideoScript::ATTRIBUTE_ID);
$table->timestamps(6);
$table->softDeletes(BaseModel::ATTRIBUTE_DELETED_AT, 6);
$table->string(VideoScript::ATTRIBUTE_PATH);
$table->unsignedBigInteger(VideoScript::ATTRIBUTE_VIDEO)->nullable();
$table->foreign(VideoScript::ATTRIBUTE_VIDEO)->references(Video::ATTRIBUTE_ID)->on(Video::TABLE)->nullOnDelete();
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists(VideoScript::TABLE);
}
};
+6 -3
View File
@@ -54,6 +54,7 @@ class PermissionSeeder extends Seeder
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'song');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'studio');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'video');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'video script');
// Special Permissions
$this->configureSpecialPermissions($admin, $wikiEditor, $wikiViewer);
@@ -108,17 +109,19 @@ class PermissionSeeder extends Seeder
$create = Permission::findOrCreate("create $wikiResource");
$adminPermissions[] = $create;
if ($wikiResource !== 'video' && $wikiResource !== 'audio') {
if ($wikiResource !== 'video' && $wikiResource !== 'audio' && $wikiResource !== 'video script') {
$editorPermissions[] = $create;
}
$update = Permission::findOrCreate("update $wikiResource");
$adminPermissions[] = $update;
$editorPermissions[] = $update;
if ($wikiResource !== 'video script') {
$editorPermissions[] = $update;
}
$delete = Permission::findOrCreate("delete $wikiResource");
$adminPermissions[] = $delete;
if ($wikiResource !== 'video' && $wikiResource !== 'audio') {
if ($wikiResource !== 'video' && $wikiResource !== 'audio' && $wikiResource !== 'video script') {
$editorPermissions[] = $delete;
}
+10
View File
@@ -73,6 +73,16 @@ class SettingSeeder extends Seeder
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG,
],
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG,
Setting::ATTRIBUTE_VALUE => 'false',
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => VideoConstants::ENCODER_VERSION,
-2
View File
@@ -1,2 +0,0 @@
*
!.gitignore
+21 -1
View File
@@ -224,7 +224,7 @@ return [
'confirmButtonText' => 'Move',
'fields' => [
'path' => [
'help' => 'The new location of the Audio. Ex: 2009/Summer/Bakemonogatari-OP1.webm.',
'help' => 'The new location of the file. Ex: 2009/Summer/Bakemonogatari-OP1.webm.',
'name' => 'Path',
],
],
@@ -284,6 +284,18 @@ return [
'name' => 'Revoke Role',
],
],
'video_script' => [
'delete' => [
'confirmText' => 'Remove Video Script from configured storage disks and from the database?',
'name' => 'Remove Video Script',
],
'move' => [
'name' => 'Move Video Script',
],
'upload' => [
'name' => 'Upload Video Script',
],
],
'video' => [
'backfill' => [
'confirmButtonText' => 'Backfill',
@@ -586,6 +598,9 @@ return [
'email' => 'Email',
'name' => 'Name',
],
'video_script' => [
'path' => 'Path',
],
'video' => [
'basename' => [
'name' => 'Basename',
@@ -693,6 +708,9 @@ return [
'resolution' => [
'name' => 'Video with Unset Resolution',
],
'script' => [
'name' => 'Video Without Script',
],
'source' => [
'name' => 'Video with Unknown Source Type',
],
@@ -759,6 +777,7 @@ return [
'studios' => 'Studios',
'transactions' => 'Transactions',
'users' => 'Users',
'video_scripts' => 'Video Scripts',
'videos' => 'Videos',
],
'singularLabel' => [
@@ -783,6 +802,7 @@ return [
'studio' => 'Studio',
'transaction' => 'Transaction',
'user' => 'User',
'video_script' => 'Video Script',
'video' => 'Video',
],
],
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
use App\Constants\Config\FlagConstants;
use App\Http\Controllers\Wiki\Video\Script\ScriptController;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
$isScriptDownloadingAllowed = Str::of('is_feature_enabled:')
->append(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED)
->append(',Script Downloading Disabled')
->__toString();
Route::get('/{videoscript}', [ScriptController::class, 'show'])
->name('videoscript.show')
->middleware([$isScriptDownloadingAllowed, 'without_trashed:videoscript']);
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Actions\Repositories\Wiki\Video\Script;
use App\Actions\Repositories\Wiki\Video\Script\ReconcileScriptRepositoriesAction;
use App\Enums\Actions\ActionStatus;
use App\Models\Wiki\Video\VideoScript;
use App\Repositories\Eloquent\Wiki\Video\ScriptRepository as ScriptDestinationRepository;
use App\Repositories\Storage\Wiki\Video\ScriptRepository as ScriptSourceRepository;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Mockery\MockInterface;
use Tests\TestCase;
/**
* Class ReconcileScriptRepositoriesTest.
*/
class ReconcileScriptRepositoriesTest extends TestCase
{
use WithFaker;
/**
* If no changes are needed, the Reconcile Script Repository Action shall indicate no changes were made.
*
* @return void
*/
public function testNoResults(): void
{
$this->mock(ScriptSourceRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('get')->once()->andReturn(Collection::make());
});
$action = new ReconcileScriptRepositoriesAction();
$source = App::make(ScriptSourceRepository::class);
$destination = App::make(ScriptDestinationRepository::class);
$result = $action->reconcileRepositories($source, $destination);
static::assertTrue(ActionStatus::PASSED()->is($result->getStatus()));
static::assertFalse($result->hasChanges());
static::assertDatabaseCount(VideoScript::class, 0);
}
/**
* If video scripts are created, the Reconcile Script Repository Action shall return created video scripts.
*
* @return void
*/
public function testCreated(): void
{
$createdScriptCount = $this->faker->numberBetween(2, 9);
$scripts = VideoScript::factory()->count($createdScriptCount)->make();
$this->mock(ScriptSourceRepository::class, function (MockInterface $mock) use ($scripts) {
$mock->shouldReceive('get')->once()->andReturn($scripts);
});
$action = new ReconcileScriptRepositoriesAction();
$source = App::make(ScriptSourceRepository::class);
$destination = App::make(ScriptDestinationRepository::class);
$result = $action->reconcileRepositories($source, $destination);
static::assertTrue(ActionStatus::PASSED()->is($result->getStatus()));
static::assertTrue($result->hasChanges());
static::assertCount($createdScriptCount, $result->getCreated());
static::assertDatabaseCount(VideoScript::class, $createdScriptCount);
}
/**
* If video scripts are deleted, the Reconcile Script Repository Action shall return deleted video scripts.
*
* @return void
*/
public function testDeleted(): void
{
$deletedScriptCount = $this->faker->numberBetween(2, 9);
$scripts = VideoScript::factory()->count($deletedScriptCount)->create();
$this->mock(ScriptSourceRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('get')->once()->andReturn(Collection::make());
});
$action = new ReconcileScriptRepositoriesAction();
$source = App::make(ScriptSourceRepository::class);
$destination = App::make(ScriptDestinationRepository::class);
$result = $action->reconcileRepositories($source, $destination);
static::assertTrue(ActionStatus::PASSED()->is($result->getStatus()));
static::assertTrue($result->hasChanges());
static::assertCount($deletedScriptCount, $result->getDeleted());
static::assertDatabaseCount(VideoScript::class, $deletedScriptCount);
foreach ($scripts as $script) {
static::assertSoftDeleted($script);
}
}
}
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\DeleteScriptAction;
use App\Constants\Config\VideoConstants;
use App\Enums\Actions\ActionStatus;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\Testing\File;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
/**
* Class DeleteScriptTest.
*/
class DeleteScriptTest extends TestCase
{
use WithFaker;
/**
* The Delete Script Action shall fail if there are no deletions.
*
* @return void
*/
public function testDefault(): void
{
Config::set(VideoConstants::SCRIPT_DISK_QUALIFIED, []);
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$script = VideoScript::factory()->createOne();
$action = new DeleteScriptAction($script);
$storageResults = $action->handle();
$result = $storageResults->toActionResult();
static::assertTrue($result->hasFailed());
}
/**
* The Delete Script Action shall pass if there are deletions.
*
* @return void
*/
public function testPassed(): void
{
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$script = VideoScript::factory()->createOne([
VideoScript::ATTRIBUTE_PATH => $file->path(),
]);
$action = new DeleteScriptAction($script);
$storageResults = $action->handle();
$result = $storageResults->toActionResult();
static::assertTrue(ActionStatus::PASSED()->is($result->getStatus()));
}
/**
* The Delete Script Action shall delete the file from the configured disks.
*
* @return void
*/
public function testDeletedFromDisk(): void
{
$fs = Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$script = VideoScript::factory()->createOne([
VideoScript::ATTRIBUTE_PATH => $file->path(),
]);
$action = new DeleteScriptAction($script);
$action->handle();
static::assertEmpty($fs->allFiles());
}
/**
* The Delete Video Action shall delete the script.
*
* @return void
*/
public function testVideoDeleted(): void
{
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$script = VideoScript::factory()->createOne([
VideoScript::ATTRIBUTE_PATH => $file->path(),
]);
$action = new DeleteScriptAction($script);
$result = $action->handle();
$action->then($result);
static::assertSoftDeleted($script);
}
}
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\MoveScriptAction;
use App\Constants\Config\VideoConstants;
use App\Enums\Actions\ActionStatus;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\Testing\File;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\TestCase;
/**
* Class MoveScriptTest.
*/
class MoveScriptTest extends TestCase
{
use WithFaker;
/**
* The Move Script Action shall fail if there are no moves.
*
* @return void
*/
public function testDefault(): void
{
Config::set(VideoConstants::SCRIPT_DISK_QUALIFIED, []);
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$script = VideoScript::factory()->createOne();
$action = new MoveScriptAction($script, $this->faker->word());
$storageResults = $action->handle();
$result = $storageResults->toActionResult();
static::assertTrue($result->hasFailed());
}
/**
* The Move Script Action shall pass if there are moves.
*
* @return void
*/
public function testPassed(): void
{
/** @var FilesystemAdapter $fs */
$fs = Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$directory = $this->faker->word();
$script = VideoScript::factory()->createOne([
VideoScript::ATTRIBUTE_PATH => $fs->putFileAs($directory, $file, $file->getClientOriginalName()),
]);
$action = new MoveScriptAction($script, Str::replace($directory, $this->faker->word(), $script->path));
$storageResults = $action->handle();
$result = $storageResults->toActionResult();
static::assertTrue(ActionStatus::PASSED()->is($result->getStatus()));
}
/**
* The Move Script Action shall move the file in the configured disks.
*
* @return void
*/
public function testMovedInDisk(): void
{
/** @var FilesystemAdapter $fs */
$fs = Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$directory = $this->faker->word();
$script = VideoScript::factory()->createOne([
VideoScript::ATTRIBUTE_PATH => $fs->putFileAs($directory, $file, $file->getClientOriginalName()),
]);
$from = $script->path;
$to = Str::replace($directory, $this->faker->word(), $script->path);
$action = new MoveScriptAction($script, $to);
$action->handle();
$fs->assertMissing($from);
$fs->assertExists($to);
}
/**
* The Move Script Action shall move the script.
*
* @return void
*/
public function testScriptUpdated(): void
{
/** @var FilesystemAdapter $fs */
$fs = Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$directory = $this->faker->word();
$script = VideoScript::factory()->createOne([
VideoScript::ATTRIBUTE_PATH => $fs->putFileAs($directory, $file, $file->getClientOriginalName()),
]);
$to = Str::replace($directory, $this->faker->word(), $script->path);
$action = new MoveScriptAction($script, $to);
$result = $action->handle();
$action->then($result);
static::assertDatabaseHas(VideoScript::class, [VideoScript::ATTRIBUTE_PATH => $to]);
}
}
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\UploadScriptAction;
use App\Constants\Config\VideoConstants;
use App\Enums\Actions\ActionStatus;
use App\Models\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\Testing\File;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
/**
* Class UploadScriptTest.
*/
class UploadScriptTest extends TestCase
{
use WithFaker;
/**
* The Upload Script Action shall fail if there are no uploads.
*
* @return void
*/
public function testDefault(): void
{
Config::set(VideoConstants::SCRIPT_DISK_QUALIFIED, []);
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$action = new UploadScriptAction($file, $this->faker->word());
$storageResults = $action->handle();
$result = $storageResults->toActionResult();
static::assertTrue($result->hasFailed());
}
/**
* The Upload Script Action shall pass if given a valid file.
*
* @return void
*/
public function testPassed(): void
{
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$action = new UploadScriptAction($file, $this->faker->word());
$storageResults = $action->handle();
$result = $storageResults->toActionResult();
static::assertTrue(ActionStatus::PASSED()->is($result->getStatus()));
}
/**
* The Upload Script Action shall upload the file to the configured disk.
*
* @return void
*/
public function testUploadedToDisk(): void
{
$fs = Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$action = new UploadScriptAction($file, $this->faker->word());
$action->handle();
static::assertCount(1, $fs->allFiles());
}
/**
* The Upload Video Action shall upload the file to the configured disk.
*
* @return void
*/
public function testCreatedVideo(): void
{
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$action = new UploadScriptAction($file, $this->faker->word());
$result = $action->handle();
$action->then($result);
static::assertDatabaseCount(VideoScript::class, 1);
}
/**
* The Upload Script Action shall attach the provided video.
*
* @return void
*/
public function testAttachesVideo(): void
{
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$video = Video::factory()->createOne();
$action = new UploadScriptAction($file, $this->faker->word(), $video);
$result = $action->handle();
$action->then($result);
static::assertTrue($video->videoscript()->exists());
}
}
@@ -11,6 +11,7 @@ use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use App\Pivots\AnimeThemeEntryVideo;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\Testing\File;
@@ -131,4 +132,27 @@ class UploadVideoTest extends TestCase
static::assertDatabaseCount(AnimeThemeEntryVideo::class, 1);
}
/**
* The Upload Video Action shall attach the provided script.
*
* @return void
*/
public function testAssociatesScript(): void
{
Storage::fake(Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED));
Config::set(VideoConstants::DISKS_QUALIFIED, [Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED)]);
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.webm', $this->faker->randomDigitNotNull());
$script = File::fake()->create($this->faker->word().'.txt', $this->faker->randomDigitNotNull());
$action = new UploadVideoAction($file, $this->faker->word(), null, $script);
$result = $action->handle();
$action->then($result);
static::assertDatabaseHas(VideoScript::class, [VideoScript::ATTRIBUTE_VIDEO => 1]);
}
}
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Console\Commands\Repositories\Storage\Wiki\Video;
use App\Console\Commands\Repositories\Storage\Wiki\Video\ScriptReconcileCommand;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use App\Repositories\Storage\Wiki\Video\ScriptRepository;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\WithoutEvents;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Mockery\MockInterface;
use Tests\TestCase;
/**
* Class ScriptReconcileTest.
*/
class ScriptReconcileTest extends TestCase
{
use WithFaker;
use WithoutEvents;
/**
* If no changes are needed, the Reconcile Script Command shall output 'No Video Scripts created or deleted or updated'.
*
* @return void
*/
public function testNoResults(): void
{
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$this->baseRefreshDatabase(); // Cannot lazily refresh database within pending command
$this->mock(ScriptRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('get')->once()->andReturn(Collection::make());
});
$this->artisan(ScriptReconcileCommand::class)
->assertSuccessful()
->expectsOutput('No Video Scripts created or deleted or updated');
}
/**
* If scripts are created, the Reconcile Script Command shall output '{Created Count} Video Scripts created, 0 Video Scripts deleted, 0 Video Scripts updated'.
*
* @return void
*/
public function testCreated(): void
{
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$this->baseRefreshDatabase(); // Cannot lazily refresh database within pending command
$createdScriptCount = $this->faker->numberBetween(2, 9);
$scripts = VideoScript::factory()->count($createdScriptCount)->make();
$this->mock(ScriptRepository::class, function (MockInterface $mock) use ($scripts) {
$mock->shouldReceive('get')->once()->andReturn($scripts);
});
$this->artisan(ScriptReconcileCommand::class)
->assertSuccessful()
->expectsOutput("$createdScriptCount Video Scripts created, 0 Video Scripts deleted, 0 Video Scripts updated");
}
/**
* If scripts are deleted, the Reconcile Script Command shall output '0 Video Scripts created, {Deleted Count} Video Scripts deleted, 0 Video Scripts updated'.
*
* @return void
*/
public function testDeleted(): void
{
Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$deletedScriptCount = $this->faker->numberBetween(2, 9);
VideoScript::factory()->count($deletedScriptCount)->create();
$this->mock(ScriptRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('get')->once()->andReturn(Collection::make());
});
$this->artisan(ScriptReconcileCommand::class)
->assertSuccessful()
->expectsOutput("0 Video Scripts created, $deletedScriptCount Video Scripts deleted, 0 Video Scripts updated");
}
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Events\Wiki\Video;
use App\Events\Wiki\Video\Script\VideoScriptCreated;
use App\Events\Wiki\Video\Script\VideoScriptDeleted;
use App\Events\Wiki\Video\Script\VideoScriptRestored;
use App\Events\Wiki\Video\Script\VideoScriptUpdated;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
/**
* Class ScriptTest.
*/
class ScriptTest extends TestCase
{
/**
* When a Script is created, a VideoScriptCreated event shall be dispatched.
*
* @return void
*/
public function testVideoScriptCreatedEventDispatched(): void
{
Event::fake();
VideoScript::factory()->createOne();
Event::assertDispatched(VideoScriptCreated::class);
}
/**
* When a Script is deleted, a VideoScriptDeleted event shall be dispatched.
*
* @return void
*/
public function testVideoScriptDeletedEventDispatched(): void
{
Event::fake();
$script = VideoScript::factory()->createOne();
$script->delete();
Event::assertDispatched(VideoScriptDeleted::class);
}
/**
* When a Script is restored, a VideoScriptRestored event shall be dispatched.
*
* @return void
*/
public function testVideoScriptRestoredEventDispatched(): void
{
Event::fake();
$script = VideoScript::factory()->createOne();
$script->restore();
Event::assertDispatched(VideoScriptRestored::class);
}
/**
* When a Script is restored, a VideoScriptUpdated event shall not be dispatched.
* Note: This is a customization that overrides default framework behavior.
* An updated event is fired on restore.
*
* @return void
*/
public function testVideoScriptRestoresQuietly(): void
{
Event::fake();
$script = VideoScript::factory()->createOne();
$script->restore();
Event::assertNotDispatched(VideoScriptUpdated::class);
}
/**
* When a Script is updated, a VideoScriptUpdated event shall be dispatched.
*
* @return void
*/
public function testVideoScriptUpdatedEventDispatched(): void
{
Event::fake();
$script = VideoScript::factory()->createOne();
$changes = VideoScript::factory()->makeOne();
$script->fill($changes->getAttributes());
$script->save();
Event::assertDispatched(VideoScriptUpdated::class);
}
/**
* The VideoScriptUpdated event shall contain embed fields.
*
* @return void
*/
public function testVideoScriptUpdatedEventEmbedFields(): void
{
Event::fake();
$script = VideoScript::factory()->createOne();
$changes = VideoScript::factory()->makeOne();
$script->fill($changes->getAttributes());
$script->save();
Event::assertDispatched(VideoScriptUpdated::class, function (VideoScriptUpdated $event) {
$message = $event->getDiscordMessage();
return ! empty(Arr::get($message->embed, 'fields'));
});
}
}
@@ -32,7 +32,7 @@ class DumpStoreTest extends TestCase
}
/**
* The Dump Store Endpoint shall require the content field.
* The Dump Store Endpoint shall require the path field.
*
* @return void
*/
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Wiki\Video\Script;
use App\Models\Auth\User;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Foundation\Testing\WithoutEvents;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class ScriptDestroyTest.
*/
class ScriptDestroyTest extends TestCase
{
use WithoutEvents;
/**
* The Script Destroy Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$script = VideoScript::factory()->createOne();
$response = $this->delete(route('api.videoscript.destroy', ['videoscript' => $script]));
$response->assertUnauthorized();
}
/**
* The Script Destroy Endpoint shall delete the script.
*
* @return void
*/
public function testDeleted(): void
{
$script = VideoScript::factory()->createOne();
$user = User::factory()->withPermission('delete video script')->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.videoscript.destroy', ['videoscript' => $script]));
$response->assertOk();
static::assertSoftDeleted($script);
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Wiki\Video\Script;
use App\Models\Auth\User;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Foundation\Testing\WithoutEvents;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class ScriptForceDeleteTest.
*/
class ScriptForceDeleteTest extends TestCase
{
use WithoutEvents;
/**
* The Script Force Destroy Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$script = VideoScript::factory()->createOne();
$response = $this->delete(route('api.videoscript.forceDelete', ['videoscript' => $script]));
$response->assertUnauthorized();
}
/**
* The Script Force Destroy Endpoint shall force delete the script.
*
* @return void
*/
public function testDeleted(): void
{
$script = VideoScript::factory()->createOne();
$user = User::factory()->withPermission('force delete video script')->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.videoscript.forceDelete', ['videoscript' => $script]));
$response->assertOk();
static::assertModelMissing($script);
}
}
@@ -0,0 +1,754 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Wiki\Video\Script;
use App\Contracts\Http\Api\Field\SortableField;
use App\Enums\Http\Api\Filter\TrashedStatus;
use App\Enums\Http\Api\Sort\Direction;
use App\Enums\Models\Wiki\VideoOverlap;
use App\Enums\Models\Wiki\VideoSource;
use App\Http\Api\Criteria\Filter\TrashedCriteria;
use App\Http\Api\Criteria\Paging\Criteria;
use App\Http\Api\Criteria\Paging\OffsetCriteria;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Parser\FieldParser;
use App\Http\Api\Parser\FilterParser;
use App\Http\Api\Parser\IncludeParser;
use App\Http\Api\Parser\PagingParser;
use App\Http\Api\Parser\SortParser;
use App\Http\Api\Query\Wiki\Video\Script\ScriptReadQuery;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Resources\Wiki\Video\Collection\ScriptCollection;
use App\Http\Resources\Wiki\Video\Resource\ScriptResource;
use App\Models\BaseModel;
use App\Models\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\WithoutEvents;
use Tests\TestCase;
/**
* Class ScriptIndexTest.
*/
class ScriptIndexTest extends TestCase
{
use WithFaker;
use WithoutEvents;
/**
* By default, the Script Index Endpoint shall return a collection of Script Resources.
*
* @return void
*/
public function testDefault(): void
{
$scripts = VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->create();
$response = $this->get(route('api.videoscript.index'));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery()))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall be paginated.
*
* @return void
*/
public function testPaginated(): void
{
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->create();
$response = $this->get(route('api.videoscript.index'));
$response->assertJsonStructure([
ScriptCollection::$wrap,
'links',
'meta',
]);
}
/**
* The Script Index Endpoint shall allow inclusion of related resources.
*
* @return void
*/
public function testAllowedIncludePaths(): void
{
$schema = new ScriptSchema();
$allowedIncludes = collect($schema->allowedIncludes());
$selectedIncludes = $allowedIncludes->random($this->faker->numberBetween(1, $allowedIncludes->count()));
$includedPaths = $selectedIncludes->map(fn (AllowedInclude $include) => $include->path());
$parameters = [
IncludeParser::param() => $includedPaths->join(','),
];
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->for(Video::factory())
->create();
$scripts = VideoScript::with($includedPaths->all())->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall implement sparse fieldsets.
*
* @return void
*/
public function testSparseFieldsets(): void
{
$schema = new ScriptSchema();
$fields = collect($schema->fields());
$includedFields = $fields->random($this->faker->numberBetween(1, $fields->count()));
$parameters = [
FieldParser::param() => [
ScriptResource::$wrap => $includedFields->map(fn (Field $field) => $field->getKey())->join(','),
],
];
$scripts = VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->create();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Synonym Index Endpoint shall support sorting resources.
*
* @return void
*/
public function testSorts(): void
{
$schema = new ScriptSchema();
$sort = collect($schema->fields())
->filter(fn (Field $field) => $field instanceof SortableField)
->map(fn (SortableField $field) => $field->getSort())
->random();
$parameters = [
SortParser::param() => $sort->format(Direction::getRandomInstance()),
];
$query = new ScriptReadQuery($parameters);
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->create();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
$query->collection($query->index())
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support filtering by created_at.
*
* @return void
*/
public function testCreatedAtFilter(): void
{
$createdFilter = $this->faker->date();
$excludedDate = $this->faker->date();
$parameters = [
FilterParser::param() => [
BaseModel::ATTRIBUTE_CREATED_AT => $createdFilter,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
Carbon::withTestNow($createdFilter, function () {
VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
});
Carbon::withTestNow($excludedDate, function () {
VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
});
$script = VideoScript::query()->where(BaseModel::ATTRIBUTE_CREATED_AT, $createdFilter)->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support filtering by updated_at.
*
* @return void
*/
public function testUpdatedAtFilter(): void
{
$updatedFilter = $this->faker->date();
$excludedDate = $this->faker->date();
$parameters = [
FilterParser::param() => [
BaseModel::ATTRIBUTE_UPDATED_AT => $updatedFilter,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
Carbon::withTestNow($updatedFilter, function () {
VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
});
Carbon::withTestNow($excludedDate, function () {
VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
});
$script = VideoScript::query()->where(BaseModel::ATTRIBUTE_UPDATED_AT, $updatedFilter)->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support filtering by trashed.
*
* @return void
*/
public function testWithoutTrashedFilter(): void
{
$parameters = [
FilterParser::param() => [
TrashedCriteria::PARAM_VALUE => TrashedStatus::WITHOUT,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
$deleteVideo = VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
$deleteVideo->each(function (VideoScript $script) {
$script->delete();
});
$script = VideoScript::withoutTrashed()->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support filtering by trashed.
*
* @return void
*/
public function testWithTrashedFilter(): void
{
$parameters = [
FilterParser::param() => [
TrashedCriteria::PARAM_VALUE => TrashedStatus::WITH,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
$deleteVideo = VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
$deleteVideo->each(function (VideoScript $script) {
$script->delete();
});
$script = VideoScript::withTrashed()->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support filtering by trashed.
*
* @return void
*/
public function testOnlyTrashedFilter(): void
{
$parameters = [
FilterParser::param() => [
TrashedCriteria::PARAM_VALUE => TrashedStatus::ONLY,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
$deleteVideo = VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
$deleteVideo->each(function (VideoScript $script) {
$script->delete();
});
$script = VideoScript::onlyTrashed()->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support filtering by deleted_at.
*
* @return void
*/
public function testDeletedAtFilter(): void
{
$deletedFilter = $this->faker->date();
$excludedDate = $this->faker->date();
$parameters = [
FilterParser::param() => [
BaseModel::ATTRIBUTE_DELETED_AT => $deletedFilter,
TrashedCriteria::PARAM_VALUE => TrashedStatus::WITH,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
Carbon::withTestNow($deletedFilter, function () {
$scripts = VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
$scripts->each(function (VideoScript $script) {
$script->delete();
});
});
Carbon::withTestNow($excludedDate, function () {
$scripts = VideoScript::factory()->count($this->faker->randomDigitNotNull())->create();
$scripts->each(function (VideoScript $script) {
$script->delete();
});
});
$script = VideoScript::withTrashed()->where(BaseModel::ATTRIBUTE_DELETED_AT, $deletedFilter)->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support constrained eager loading of videos by lyrics.
*
* @return void
*/
public function testVideosByLyrics(): void
{
$lyricsFilter = $this->faker->boolean();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_LYRICS => $lyricsFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->for(Video::factory())
->create();
$scripts = VideoScript::with([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($lyricsFilter) {
$query->where(Video::ATTRIBUTE_LYRICS, $lyricsFilter);
},
])
->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support constrained eager loading of videos by nc.
*
* @return void
*/
public function testVideosByNc(): void
{
$ncFilter = $this->faker->boolean();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_NC => $ncFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->for(Video::factory())
->create();
$scripts = VideoScript::with([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($ncFilter) {
$query->where(Video::ATTRIBUTE_NC, $ncFilter);
},
])
->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support constrained eager loading of videos by overlap.
*
* @return void
*/
public function testVideosByOverlap(): void
{
$overlapFilter = VideoOverlap::getRandomInstance();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_OVERLAP => $overlapFilter->description,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->for(Video::factory())
->create();
$scripts = VideoScript::with([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($overlapFilter) {
$query->where(Video::ATTRIBUTE_OVERLAP, $overlapFilter->value);
},
])
->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support constrained eager loading of videos by resolution.
*
* @return void
*/
public function testVideosByResolution(): void
{
$resolutionFilter = $this->faker->randomNumber();
$excludedResolution = $resolutionFilter + 1;
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_RESOLUTION => $resolutionFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->for(
Video::factory()->state([
Video::ATTRIBUTE_RESOLUTION => $this->faker->boolean() ? $resolutionFilter : $excludedResolution,
])
)
->create();
$scripts = VideoScript::with([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($resolutionFilter) {
$query->where(Video::ATTRIBUTE_RESOLUTION, $resolutionFilter);
},
])
->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support constrained eager loading of videos by source.
*
* @return void
*/
public function testVideosBySource(): void
{
$sourceFilter = VideoSource::getRandomInstance();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_SOURCE => $sourceFilter->description,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->for(Video::factory())
->create();
$scripts = VideoScript::with([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($sourceFilter) {
$query->where(Video::ATTRIBUTE_SOURCE, $sourceFilter->value);
},
])
->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support constrained eager loading of videos by subbed.
*
* @return void
*/
public function testVideosBySubbed(): void
{
$subbedFilter = $this->faker->boolean();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_SUBBED => $subbedFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->for(Video::factory())
->create();
$scripts = VideoScript::with([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($subbedFilter) {
$query->where(Video::ATTRIBUTE_SUBBED, $subbedFilter);
},
])
->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Index Endpoint shall support constrained eager loading of videos by uncen.
*
* @return void
*/
public function testVideosByUncen(): void
{
$uncenFilter = $this->faker->boolean();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_UNCEN => $uncenFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
VideoScript::factory()
->count($this->faker->randomDigitNotNull())
->for(Video::factory())
->create();
$scripts = VideoScript::with([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($uncenFilter) {
$query->where(Video::ATTRIBUTE_UNCEN, $uncenFilter);
},
])
->get();
$response = $this->get(route('api.videoscript.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptCollection($scripts, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Wiki\Video\Script;
use App\Models\Auth\User;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Foundation\Testing\WithoutEvents;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class ScriptRestoreTest.
*/
class ScriptRestoreTest extends TestCase
{
use WithoutEvents;
/**
* The Script Restore Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$script = VideoScript::factory()->createOne();
$script->delete();
$response = $this->patch(route('api.videoscript.restore', ['videoscript' => $script]));
$response->assertUnauthorized();
}
/**
* The Script Restore Endpoint shall restore the script.
*
* @return void
*/
public function testRestored(): void
{
$script = VideoScript::factory()->createOne();
$script->delete();
$user = User::factory()->withPermission('restore video script')->createOne();
Sanctum::actingAs($user);
$response = $this->patch(route('api.videoscript.restore', ['videoscript' => $script]));
$response->assertOk();
static::assertNotSoftDeleted($script);
}
}
@@ -0,0 +1,436 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Wiki\Video\Script;
use App\Enums\Models\Wiki\VideoOverlap;
use App\Enums\Models\Wiki\VideoSource;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Parser\FieldParser;
use App\Http\Api\Parser\FilterParser;
use App\Http\Api\Parser\IncludeParser;
use App\Http\Api\Query\Wiki\Video\Script\ScriptReadQuery;
use App\Http\Api\Schema\Wiki\Video\ScriptSchema;
use App\Http\Resources\Wiki\Video\Resource\ScriptResource;
use App\Models\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\WithoutEvents;
use Tests\TestCase;
/**
* Class ScriptShowTest.
*/
class ScriptShowTest extends TestCase
{
use WithFaker;
use WithoutEvents;
/**
* By default, the Script Show Endpoint shall return a Script Resource.
*
* @return void
*/
public function testDefault(): void
{
$script = VideoScript::factory()->create();
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script]));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery()))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall return a Script Resource for soft deleted scripts.
*
* @return void
*/
public function testSoftDelete(): void
{
$script = VideoScript::factory()->createOne();
$script->delete();
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script]));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery()))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall allow inclusion of related resources.
*
* @return void
*/
public function testAllowedIncludePaths(): void
{
$schema = new ScriptSchema();
$allowedIncludes = collect($schema->allowedIncludes());
$selectedIncludes = $allowedIncludes->random($this->faker->numberBetween(1, $allowedIncludes->count()));
$includedPaths = $selectedIncludes->map(fn (AllowedInclude $include) => $include->path());
$parameters = [
IncludeParser::param() => $includedPaths->join(','),
];
$script = VideoScript::factory()
->for(Video::factory())
->createOne();
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall implement sparse fieldsets.
*
* @return void
*/
public function testSparseFieldsets(): void
{
$schema = new ScriptSchema();
$fields = collect($schema->fields());
$includedFields = $fields->random($this->faker->numberBetween(1, $fields->count()));
$parameters = [
FieldParser::param() => [
ScriptResource::$wrap => $includedFields->map(fn (Field $field) => $field->getKey())->join(','),
],
];
$script = VideoScript::factory()->create();
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall support constrained eager loading of videos by lyrics.
*
* @return void
*/
public function testVideosByLyrics(): void
{
$lyricsFilter = $this->faker->boolean();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_LYRICS => $lyricsFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
$script = VideoScript::factory()
->for(Video::factory())
->create();
$script->unsetRelations()->load([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($lyricsFilter) {
$query->where(Video::ATTRIBUTE_LYRICS, $lyricsFilter);
},
]);
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall support constrained eager loading of videos by nc.
*
* @return void
*/
public function testVideosByNc(): void
{
$ncFilter = $this->faker->boolean();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_NC => $ncFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
$script = VideoScript::factory()
->for(Video::factory())
->create();
$script->unsetRelations()->load([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($ncFilter) {
$query->where(Video::ATTRIBUTE_NC, $ncFilter);
},
]);
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall support constrained eager loading of videos by overlap.
*
* @return void
*/
public function testVideosByOverlap(): void
{
$overlapFilter = VideoOverlap::getRandomInstance();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_OVERLAP => $overlapFilter->description,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
$script = VideoScript::factory()
->for(Video::factory())
->create();
$script->unsetRelations()->load([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($overlapFilter) {
$query->where(Video::ATTRIBUTE_OVERLAP, $overlapFilter->value);
},
]);
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall support constrained eager loading of videos by resolution.
*
* @return void
*/
public function testVideosByResolution(): void
{
$resolutionFilter = $this->faker->randomNumber();
$excludedResolution = $resolutionFilter + 1;
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_RESOLUTION => $resolutionFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
$script = VideoScript::factory()
->for(
Video::factory()->state([
Video::ATTRIBUTE_RESOLUTION => $this->faker->boolean() ? $resolutionFilter : $excludedResolution,
])
)
->create();
$script->unsetRelations()->load([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($resolutionFilter) {
$query->where(Video::ATTRIBUTE_RESOLUTION, $resolutionFilter);
},
]);
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall support constrained eager loading of videos by source.
*
* @return void
*/
public function testVideosBySource(): void
{
$sourceFilter = VideoSource::getRandomInstance();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_SOURCE => $sourceFilter->description,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
$script = VideoScript::factory()
->for(Video::factory())
->create();
$script->unsetRelations()->load([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($sourceFilter) {
$query->where(Video::ATTRIBUTE_SOURCE, $sourceFilter->value);
},
]);
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall support constrained eager loading of videos by subbed.
*
* @return void
*/
public function testVideosBySubbed(): void
{
$subbedFilter = $this->faker->boolean();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_SUBBED => $subbedFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
$script = VideoScript::factory()
->for(Video::factory())
->create();
$script->unsetRelations()->load([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($subbedFilter) {
$query->where(Video::ATTRIBUTE_SUBBED, $subbedFilter);
},
]);
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Script Show Endpoint shall support constrained eager loading of videos by uncen.
*
* @return void
*/
public function testVideosByUncen(): void
{
$uncenFilter = $this->faker->boolean();
$parameters = [
FilterParser::param() => [
Video::ATTRIBUTE_UNCEN => $uncenFilter,
],
IncludeParser::param() => VideoScript::RELATION_VIDEO,
];
$script = VideoScript::factory()
->for(Video::factory())
->create();
$script->unsetRelations()->load([
VideoScript::RELATION_VIDEO => function (BelongsTo $query) use ($uncenFilter) {
$query->where(Video::ATTRIBUTE_UNCEN, $uncenFilter);
},
]);
$response = $this->get(route('api.videoscript.show', ['videoscript' => $script] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ScriptResource($script, new ScriptReadQuery($parameters)))
->response()
->getData()
),
true
)
);
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Wiki\Video\Script;
use App\Models\Auth\User;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Foundation\Testing\WithoutEvents;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class ScriptStoreTest.
*/
class ScriptStoreTest extends TestCase
{
use WithoutEvents;
/**
* The Script Store Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$script = VideoScript::factory()->makeOne();
$response = $this->post(route('api.videoscript.store', $script->toArray()));
$response->assertUnauthorized();
}
/**
* The Script Store Endpoint shall require the path field.
*
* @return void
*/
public function testRequiredFields(): void
{
$user = User::factory()->withPermission('create video script')->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.videoscript.store'));
$response->assertJsonValidationErrors([
VideoScript::ATTRIBUTE_PATH,
]);
}
/**
* The Script Store Endpoint shall create a script.
*
* @return void
*/
public function testCreate(): void
{
$parameters = VideoScript::factory()->raw();
$user = User::factory()->withPermission('create video script')->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.videoscript.store', $parameters));
$response->assertCreated();
static::assertDatabaseCount(VideoScript::TABLE, 1);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Wiki\Video\Script;
use App\Models\Auth\User;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Foundation\Testing\WithoutEvents;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class ScriptUpdateTest.
*/
class ScriptUpdateTest extends TestCase
{
use WithoutEvents;
/**
* The Script Update Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$script = VideoScript::factory()->createOne();
$parameters = VideoScript::factory()->raw();
$response = $this->put(route('api.videoscript.update', ['videoscript' => $script] + $parameters));
$response->assertUnauthorized();
}
/**
* The Script Update Endpoint shall update a script.
*
* @return void
*/
public function testUpdate(): void
{
$script = VideoScript::factory()->createOne();
$parameters = VideoScript::factory()->raw();
$user = User::factory()->withPermission('update video script')->createOne();
Sanctum::actingAs($user);
$response = $this->put(route('api.videoscript.update', ['videoscript' => $script] + $parameters));
$response->assertOk();
}
}
@@ -31,6 +31,7 @@ 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\Models\Wiki\Video\VideoScript;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -114,6 +115,7 @@ class VideoIndexTest extends TestCase
Video::factory()
->count($this->faker->randomDigitNotNull())
->for(Audio::factory())
->has(VideoScript::factory(), 'videoscript')
->has(
AnimeThemeEntry::factory()
->count($this->faker->randomDigitNotNull())
@@ -19,6 +19,7 @@ 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\Models\Wiki\Video\VideoScript;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -103,6 +104,7 @@ class VideoShowTest extends TestCase
$video = Video::factory()
->for(Audio::factory())
->has(VideoScript::factory(), 'videoscript')
->has(
AnimeThemeEntry::factory()
->count($this->faker->randomDigitNotNull())
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\Feature\Http\Wiki;
namespace Tests\Feature\Http\Wiki\Audio;
use App\Constants\Config\FlagConstants;
use App\Models\Wiki\Audio;
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Wiki\Video\Script;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\WithoutEvents;
use Illuminate\Http\Testing\File;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Tests\TestCase;
/**
* Class ScriptTest.
*/
class ScriptTest extends TestCase
{
use WithFaker;
use WithoutEvents;
/**
* If script downloading is disabled through the 'flags.allow_script_downloading' property,
* the user shall receive a forbidden exception.
*
* @return void
*/
public function testScriptDownloadingNotAllowedForbidden(): void
{
Config::set(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED, false);
$script = VideoScript::factory()->createOne();
$response = $this->get(route('videoscript.show', ['videoscript' => $script]));
$response->assertForbidden();
}
/**
* If the script is soft-deleted, the user shall receive a forbidden exception.
*
* @return void
*/
public function testSoftDeleteScriptDownloadingForbidden(): void
{
Config::set(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED, false);
$script = VideoScript::factory()->createOne();
$script->delete();
$response = $this->get(route('videoscript.show', ['videoscript' => $script]));
$response->assertForbidden();
}
/**
* If script downloading is enabled, the script is downloaded from storage through the response.
*
* @return void
*/
public function testDownloadedThroughResponse(): void
{
Config::set(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED, true);
$fs = Storage::fake(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
$file = File::fake()->create($this->faker->word().'.txt');
$fsFile = $fs->putFile('', $file);
$script = VideoScript::factory()->createOne([
VideoScript::ATTRIBUTE_PATH => $fsFile,
]);
$response = $this->get(route('videoscript.show', ['videoscript' => $script]));
static::assertInstanceOf(StreamedResponse::class, $response->baseResponse);
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\Feature\Http\Wiki;
namespace Tests\Feature\Http\Wiki\Video;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\VideoConstants;
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Jobs\Wiki\Video;
use App\Constants\Config\FlagConstants;
use App\Jobs\SendDiscordNotificationJob;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Config;
use Tests\TestCase;
/**
* Class ScriptTest.
*/
class ScriptTest extends TestCase
{
/**
* When a script is created, a SendDiscordNotification job shall be dispatched.
*
* @return void
*/
public function testVideoCreatedSendsDiscordNotification(): void
{
Config::set(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED, true);
Bus::fake(SendDiscordNotificationJob::class);
VideoScript::factory()->createOne();
Bus::assertDispatched(SendDiscordNotificationJob::class);
}
/**
* When a script is deleted, a SendDiscordNotification job shall be dispatched.
*
* @return void
*/
public function testVideoDeletedSendsDiscordNotification(): void
{
$script = VideoScript::factory()->createOne();
Config::set(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED, true);
Bus::fake(SendDiscordNotificationJob::class);
$script->delete();
Bus::assertDispatched(SendDiscordNotificationJob::class);
}
/**
* When a script is restored, a SendDiscordNotification job shall be dispatched.
*
* @return void
*/
public function testVideoRestoredSendsDiscordNotification(): void
{
$script = VideoScript::factory()->createOne();
Config::set(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED, true);
Bus::fake(SendDiscordNotificationJob::class);
$script->restore();
Bus::assertDispatched(SendDiscordNotificationJob::class);
}
/**
* When a script is updated, a SendDiscordNotification job shall be dispatched.
*
* @return void
*/
public function testVideoUpdatedSendsDiscordNotification(): void
{
$script = VideoScript::factory()->createOne();
Config::set(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED, true);
Bus::fake(SendDiscordNotificationJob::class);
$changes = VideoScript::factory()->makeOne();
$script->fill($changes->getAttributes());
$script->save();
Bus::assertDispatched(SendDiscordNotificationJob::class);
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Models\Wiki\Video;
use App\Models\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Tests\TestCase;
/**
* Class ScriptTest.
*/
class ScriptTest extends TestCase
{
/**
* Dumps shall be nameable.
*
* @return void
*/
public function testNameable(): void
{
$script = VideoScript::factory()->createOne();
static::assertIsString($script->getName());
}
/**
* Scripts shall belong to a Video.
*
* @return void
*/
public function testVideo(): void
{
$script = VideoScript::factory()
->for(Video::factory())
->createOne();
static::assertInstanceOf(BelongsTo::class, $script->video());
static::assertInstanceOf(Video::class, $script->video()->first());
}
}
+17
View File
@@ -11,10 +11,12 @@ 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\Models\Wiki\Video\VideoScript;
use App\Pivots\AnimeThemeEntryVideo;
use CyrildeWit\EloquentViewable\View;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
@@ -309,6 +311,21 @@ class VideoTest extends TestCase
static::assertInstanceOf(Audio::class, $video->audio()->first());
}
/**
* Video shall have a one-to-one relationship with the type Script.
*
* @return void
*/
public function testScript(): void
{
$video = Video::factory()
->has(VideoScript::factory(), 'videoscript')
->createOne();
static::assertInstanceOf(HasOne::class, $video->videoscript());
static::assertInstanceOf(VideoScript::class, $video->videoscript()->first());
}
/**
* Provider for source priority testing.
*