mirror of
https://github.com/AnimeThemes/animethemes-server.git
synced 2026-07-11 01:24:46 +02:00
feat(admin): upload video action (#436)
* feat(admin): upload video action * style: fix StyleCI findings * tests: fix video storage tests
This commit is contained in:
+19
-8
@@ -98,14 +98,23 @@ IMAGE_DISABLE_ASSERTS=
|
||||
IMAGE_VISIBILITY=
|
||||
IMAGE_URL=
|
||||
|
||||
VIDEO_ACCESS_KEY_ID=
|
||||
VIDEO_SECRET_ACCESS_KEY=
|
||||
VIDEO_DEFAULT_REGION=
|
||||
VIDEO_ENDPOINT=
|
||||
VIDEO_BUCKET=
|
||||
VIDEO_STREAM_READS=
|
||||
VIDEO_DISABLE_ASSERTS=
|
||||
VIDEO_VISIBILITY=
|
||||
VIDEO_NYC_ACCESS_KEY_ID=
|
||||
VIDEO_NYC_SECRET_ACCESS_KEY=
|
||||
VIDEO_NYC_DEFAULT_REGION=
|
||||
VIDEO_NYC_ENDPOINT=
|
||||
VIDEO_NYC_BUCKET=
|
||||
VIDEO_NYC_STREAM_READS=
|
||||
VIDEO_NYC_DISABLE_ASSERTS=
|
||||
VIDEO_NYC_VISIBILITY=
|
||||
|
||||
VIDEO_FRA_ACCESS_KEY_ID=
|
||||
VIDEO_FRA_SECRET_ACCESS_KEY=
|
||||
VIDEO_FRA_DEFAULT_REGION=
|
||||
VIDEO_FRA_ENDPOINT=
|
||||
VIDEO_FRA_BUCKET=
|
||||
VIDEO_FRA_STREAM_READS=
|
||||
VIDEO_FRA_DISABLE_ASSERTS=
|
||||
VIDEO_FRA_VISIBILITY=
|
||||
|
||||
AUDIO_ACCESS_KEY_ID=
|
||||
AUDIO_SECRET_ACCESS_KEY=
|
||||
@@ -217,6 +226,8 @@ VIDEO_PATH=/video
|
||||
VIDEO_URL=
|
||||
VIDEO_STREAMING_METHOD=
|
||||
VIDEO_NGINX_REDIRECT=
|
||||
VIDEO_ENCODER_VERSION=
|
||||
VIDEO_UPLOAD_DISKS=
|
||||
|
||||
# web
|
||||
WEB_URL=http://localhost
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Video;
|
||||
|
||||
use App\Actions\Models\ActionResult;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class UploadResults.
|
||||
*/
|
||||
class UploadResults
|
||||
{
|
||||
/**
|
||||
* Create a new action result instance.
|
||||
*
|
||||
* @param array<string, string|false> $uploads
|
||||
*/
|
||||
public function __construct(protected readonly array $uploads = [])
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Write results to log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
if (empty($this->uploads)) {
|
||||
Log::error('No uploads were attempted.');
|
||||
}
|
||||
foreach ($this->uploads as $fs => $result) {
|
||||
$result === false
|
||||
? Log::error("Failed to upload to disk '$fs'")
|
||||
: Log::info("Uploaded '$result' to disk '$fs'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to Action Result.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function toActionResult(): ActionResult
|
||||
{
|
||||
if (empty($this->uploads)) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
'No uploads were attempted. Please check that upload disks are configured.'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var Collection $failed */
|
||||
/** @var Collection $passed */
|
||||
[$failed, $passed] = collect($this->uploads)->partition(fn (string|false $result, string $fs) => $result === false);
|
||||
|
||||
if ($failed->isNotEmpty()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"Failed to upload to disks {$failed->keys()->join(', ', '& ')}."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED(),
|
||||
"Uploaded '{$passed->values()->first()}' to disks {$passed->keys()->join(', ', '& ')}."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Video;
|
||||
|
||||
use App\Actions\Models\ActionResult;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Actions\Repositories\Wiki\Video\ReconcileVideoRepositories;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
|
||||
use App\Repositories\Storage\Wiki\VideoRepository as VideoSourceRepository;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Class UploadVideoAction.
|
||||
*/
|
||||
class UploadVideoAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @param string $path
|
||||
*/
|
||||
public function __construct(protected readonly UploadedFile $file, protected readonly string $path)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
$uploadResults = $this->upload();
|
||||
|
||||
$uploadResults->toLog();
|
||||
|
||||
$reconcileResults = $this->reconcileVideo();
|
||||
|
||||
$reconcileResults->toLog();
|
||||
|
||||
return $uploadResults->toActionResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload the video to configured disks.
|
||||
*
|
||||
* @return UploadResults
|
||||
*/
|
||||
protected function upload(): UploadResults
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$uploadDisks = Config::get('video.upload_disks');
|
||||
foreach ($uploadDisks as $uploadDisk) {
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk($uploadDisk);
|
||||
|
||||
$result = $fs->putFileAs($this->path, $this->file, $this->file->getClientOriginalName());
|
||||
|
||||
$results[$uploadDisk] = $result;
|
||||
}
|
||||
|
||||
return new UploadResults($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile video repository.
|
||||
*
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
protected function reconcileVideo(): ReconcileResults
|
||||
{
|
||||
$action = new ReconcileVideoRepositories();
|
||||
|
||||
/** @var RepositoryInterface $sourceRepository */
|
||||
$sourceRepository = App::make(VideoSourceRepository::class);
|
||||
$sourceRepository->handleFilter('path', $this->path);
|
||||
|
||||
/** @var RepositoryInterface $destinationRepository */
|
||||
$destinationRepository = App::make(VideoDestinationRepository::class);
|
||||
$destinationRepository->handleFilter('path', $this->path);
|
||||
|
||||
return $action->reconcileRepositories($sourceRepository, $destinationRepository);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use App\Contracts\Http\Api\Field\CreatableField;
|
||||
use App\Contracts\Http\Api\Field\UpdatableField;
|
||||
use App\Http\Api\Field\StringField;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Rules\Wiki\ResourceLinkMatchesSiteRule;
|
||||
use App\Rules\Wiki\Resource\ResourceLinkMatchesSiteRule;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\Contracts\Http\Api\Field\UpdatableField;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Http\Api\Field\EnumField;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Rules\Wiki\ResourceSiteMatchesLinkRule;
|
||||
use App\Rules\Wiki\Resource\ResourceSiteMatchesLinkRule;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Nova\Actions\Wiki;
|
||||
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Rules\Wiki\ResourceLinkMatchesSiteRule;
|
||||
use App\Rules\Wiki\Resource\ResourceLinkMatchesSiteRule;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Nova\Actions\Action;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Nova\Actions\Wiki\Video;
|
||||
|
||||
use App\Actions\Models\Wiki\Video\UploadVideoAction as UploadVideo;
|
||||
use App\Rules\Wiki\StorageDirectoryExistsRule;
|
||||
use App\Rules\Wiki\Submission\Audio\AudioChannelLayoutStreamRule;
|
||||
use App\Rules\Wiki\Submission\Audio\AudioChannelsStreamRule;
|
||||
use App\Rules\Wiki\Submission\Audio\AudioCodecStreamRule;
|
||||
use App\Rules\Wiki\Submission\Audio\AudioIndexStreamRule;
|
||||
use App\Rules\Wiki\Submission\Audio\AudioLoudnessIntegratedTargetStreamRule;
|
||||
use App\Rules\Wiki\Submission\Audio\AudioLoudnessTruePeakStreamRule;
|
||||
use App\Rules\Wiki\Submission\Audio\AudioSampleRateStreamRule;
|
||||
use App\Rules\Wiki\Submission\Format\BitrateRestrictionFormatRule;
|
||||
use App\Rules\Wiki\Submission\Format\EncoderNameFormatRule;
|
||||
use App\Rules\Wiki\Submission\Format\EncoderVersionFormatRule;
|
||||
use App\Rules\Wiki\Submission\Format\ExtraneousChaptersFormatRule;
|
||||
use App\Rules\Wiki\Submission\Format\ExtraneousMetadataFormatRule;
|
||||
use App\Rules\Wiki\Submission\Format\FormatNameFormatRule;
|
||||
use App\Rules\Wiki\Submission\Format\TotalStreamsFormatRule;
|
||||
use App\Rules\Wiki\Submission\Video\VideoCodecStreamRule;
|
||||
use App\Rules\Wiki\Submission\Video\VideoColorPrimariesStreamRule;
|
||||
use App\Rules\Wiki\Submission\Video\VideoColorSpaceStreamRule;
|
||||
use App\Rules\Wiki\Submission\Video\VideoColorTransferStreamRule;
|
||||
use App\Rules\Wiki\Submission\Video\VideoIndexStreamRule;
|
||||
use App\Rules\Wiki\Submission\Video\VideoPixelFormatStreamRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rules\File as FileRule;
|
||||
use Laravel\Nova\Actions\Action;
|
||||
use Laravel\Nova\Fields\ActionFields;
|
||||
use Laravel\Nova\Fields\File;
|
||||
use Laravel\Nova\Fields\Text;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
/**
|
||||
* Class UploadVideoAction.
|
||||
*/
|
||||
class UploadVideoAction extends Action
|
||||
{
|
||||
/**
|
||||
* Get the displayable name of the action.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return __('nova.upload_video');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the action on the given models.
|
||||
*
|
||||
* @param ActionFields $fields
|
||||
* @param Collection $models
|
||||
* @return array
|
||||
*
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
public function handle(ActionFields $fields, Collection $models): array
|
||||
{
|
||||
/** @var UploadedFile $video */
|
||||
$video = $fields->get('video');
|
||||
$path = $fields->get('path');
|
||||
|
||||
$action = new UploadVideo($video, $path);
|
||||
|
||||
$result = $action->handle();
|
||||
|
||||
if ($result->hasFailed()) {
|
||||
return Action::danger($result->getMessage());
|
||||
}
|
||||
|
||||
return Action::message($result->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields available on the action.
|
||||
*
|
||||
* @param NovaRequest $request
|
||||
* @return array
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function fields(NovaRequest $request): array
|
||||
{
|
||||
$fs = Storage::disk(Config::get('video.disk'));
|
||||
|
||||
return [
|
||||
File::make(__('nova.video'), 'video')
|
||||
->required()
|
||||
->rules([
|
||||
'required',
|
||||
FileRule::types('webm')->max(200 * 1024),
|
||||
new TotalStreamsFormatRule(),
|
||||
new EncoderNameFormatRule(),
|
||||
new EncoderVersionFormatRule(),
|
||||
new FormatNameFormatRule(),
|
||||
new BitrateRestrictionFormatRule(),
|
||||
new ExtraneousMetadataFormatRule(),
|
||||
new ExtraneousChaptersFormatRule(),
|
||||
new AudioIndexStreamRule(),
|
||||
new AudioCodecStreamRule(),
|
||||
new AudioSampleRateStreamRule(),
|
||||
new AudioChannelsStreamRule(),
|
||||
new AudioChannelLayoutStreamRule(),
|
||||
new AudioLoudnessTruePeakStreamRule(),
|
||||
new AudioLoudnessIntegratedTargetStreamRule(),
|
||||
new VideoIndexStreamRule(),
|
||||
new VideoCodecStreamRule(),
|
||||
new VideoPixelFormatStreamRule(),
|
||||
new VideoColorSpaceStreamRule(),
|
||||
new VideoColorTransferStreamRule(),
|
||||
new VideoColorPrimariesStreamRule(),
|
||||
])
|
||||
->help(__('nova.upload_video_help')),
|
||||
|
||||
Text::make(__('nova.path'), 'path')
|
||||
->required()
|
||||
->rules(['required', 'string', 'doesnt_start_with:/', new StorageDirectoryExistsRule($fs)])
|
||||
->help(__('nova.upload_video_path_help')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use App\Pivots\AnimeResource;
|
||||
use App\Pivots\ArtistResource;
|
||||
use App\Pivots\BasePivot;
|
||||
use App\Pivots\StudioResource;
|
||||
use App\Rules\Wiki\ResourceLinkMatchesSiteRule;
|
||||
use App\Rules\Wiki\Resource\ResourceLinkMatchesSiteRule;
|
||||
use BenSampo\Enum\Enum;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Video as VideoModel;
|
||||
use App\Nova\Actions\Wiki\Video\BackfillAudioAction;
|
||||
use App\Nova\Actions\Wiki\Video\ReconcileVideoAction;
|
||||
use App\Nova\Actions\Wiki\Video\UploadVideoAction;
|
||||
use App\Nova\Lenses\Video\VideoAudioLens;
|
||||
use App\Nova\Lenses\Video\VideoResolutionLens;
|
||||
use App\Nova\Lenses\Video\VideoSourceLens;
|
||||
@@ -282,6 +283,17 @@ class Video extends BaseResource
|
||||
return $user instanceof User && $user->can('update video');
|
||||
}),
|
||||
|
||||
(new UploadVideoAction())
|
||||
->confirmButtonText(__('nova.upload'))
|
||||
->cancelButtonText(__('nova.cancel'))
|
||||
->onlyOnIndex()
|
||||
->standalone()
|
||||
->canSee(function (Request $request) {
|
||||
$user = $request->user();
|
||||
|
||||
return $user instanceof User && $user->can('create video');
|
||||
}),
|
||||
|
||||
(new ReconcileVideoAction())
|
||||
->confirmButtonText(__('nova.reconcile'))
|
||||
->cancelButtonText(__('nova.cancel'))
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki;
|
||||
namespace App\Rules\Wiki\Resource;
|
||||
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki;
|
||||
namespace App\Rules\Wiki\Resource;
|
||||
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Audio;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class AudioChannelLayoutStreamRule.
|
||||
*/
|
||||
class AudioChannelLayoutStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$audio = $this->streams()
|
||||
->audios()
|
||||
->first();
|
||||
|
||||
return $audio !== null && $audio->get('channel_layout') === 'stereo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.audio_channel_layout');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Audio;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class AudioChannelsStreamRule.
|
||||
*/
|
||||
class AudioChannelsStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$audio = $this->streams()
|
||||
->audios()
|
||||
->first();
|
||||
|
||||
return $audio !== null && $audio->get('channels') === 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.audio_channels');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Audio;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class AudioCodecStreamRule.
|
||||
*/
|
||||
class AudioCodecStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$audio = $this->streams()
|
||||
->audios()
|
||||
->first();
|
||||
|
||||
return $audio !== null && $audio->get('codec_name') === 'opus';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.audio_codec');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Audio;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use FFMpeg\FFProbe\DataMapping\Stream;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class AudioIndexStreamRule.
|
||||
*/
|
||||
class AudioIndexStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$streams = $this->streams()->all();
|
||||
|
||||
return collect($streams)->contains(fn (Stream $stream) => $stream->isAudio() && $stream->get('index') === 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.audio_index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Audio;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* Class AudioLoudnessIntegratedTargetStreamRule.
|
||||
*/
|
||||
class AudioLoudnessIntegratedTargetStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$loudness = $this->loudness();
|
||||
|
||||
$target = floatval(Arr::get($loudness, 'input_i'));
|
||||
|
||||
return $target >= -17.0 && $target <= -14.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.audio_loudness_integrated_target');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Audio;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* Class AudioLoudnessTruePeakStreamRule.
|
||||
*/
|
||||
class AudioLoudnessTruePeakStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$loudness = $this->loudness();
|
||||
|
||||
$peak = floatval(Arr::get($loudness, 'input_tp'));
|
||||
|
||||
return $peak <= 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.audio_loudness_true_peak');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Audio;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class AudioSampleRateStreamRule.
|
||||
*/
|
||||
class AudioSampleRateStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$audio = $this->streams()
|
||||
->audios()
|
||||
->first();
|
||||
|
||||
return $audio !== null && $audio->get('sample_rate') === '48000';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.audio_sample_rate');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Format;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* Class BitrateRestrictionFormatRule.
|
||||
*/
|
||||
class BitrateRestrictionFormatRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$format = $this->format()->all();
|
||||
|
||||
$video = $this->streams()
|
||||
->videos()
|
||||
->first();
|
||||
|
||||
$bitrate = intval(Arr::get($format, 'bit_rate'));
|
||||
$height = $video->getDimensions()->getHeight();
|
||||
|
||||
// Linear approximation of egregious bitrate by resolution
|
||||
return $bitrate < $height * 7100 + 475000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.format_bitrate_restriction');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Format;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class EncoderNameFormatRule.
|
||||
*/
|
||||
class EncoderNameFormatRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$format = $this->format()->all();
|
||||
|
||||
$tags = Arr::get($format, 'tags');
|
||||
|
||||
$tags = array_change_key_case($tags);
|
||||
|
||||
$encoder = Arr::get($tags, 'encoder');
|
||||
|
||||
return Str::startsWith($encoder, 'Lavf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.format_encoder_name');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Format;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* Class EncoderVersionFormatRule.
|
||||
*/
|
||||
class EncoderVersionFormatRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$format = $this->format()->all();
|
||||
|
||||
$tags = Arr::get($format, 'tags');
|
||||
|
||||
$tags = array_change_key_case($tags);
|
||||
|
||||
$encoder = Arr::get($tags, 'encoder');
|
||||
|
||||
return version_compare($encoder, Config::get('video.encoder_version'), '>=');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.format_encoder_version');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Format;
|
||||
|
||||
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class ExtraneousChaptersRule.
|
||||
*/
|
||||
class ExtraneousChaptersFormatRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*
|
||||
* @throws ExecutionFailureException
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
return empty($this->chapters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.format_extraneous_chapters');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Format;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* Class ExtraneousMetadataFormatRule.
|
||||
*/
|
||||
class ExtraneousMetadataFormatRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$format = $this->format()->all();
|
||||
|
||||
$tags = Arr::get($format, 'tags');
|
||||
|
||||
$tags = array_change_key_case($tags);
|
||||
|
||||
return collect($tags)->keys()->diff(['encoder', 'duration'])->isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.format_extraneous_metadata');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Format;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* Class FormatNameFormatRule.
|
||||
*/
|
||||
class FormatNameFormatRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$format = $this->format()->all();
|
||||
|
||||
$formatName = Arr::get($format, 'format_name');
|
||||
|
||||
return $formatName === 'matroska,webm';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.format_format_name');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Format;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class TotalStreamsFormatRule.
|
||||
*/
|
||||
class TotalStreamsFormatRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$streams = $this->streams();
|
||||
|
||||
return $streams->count() === 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.format_total_streams');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission;
|
||||
|
||||
use FFMpeg\FFProbe\DataMapping\Format;
|
||||
use FFMpeg\FFProbe\DataMapping\StreamCollection;
|
||||
use FFMpeg\FFProbe\Mapper;
|
||||
use Illuminate\Contracts\Validation\DataAwareRule;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Contracts\Validation\ValidatorAwareRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Validator;
|
||||
use ProtoneMedia\LaravelFFMpeg\FFMpeg\FFProbe;
|
||||
use ProtoneMedia\LaravelFFMpeg\Support\FFMpeg;
|
||||
|
||||
/**
|
||||
* Class SubmissionRule.
|
||||
*/
|
||||
abstract class SubmissionRule implements DataAwareRule, Rule, ValidatorAwareRule
|
||||
{
|
||||
/**
|
||||
* The data under validation.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* Set the data under validation.
|
||||
*
|
||||
* @param array $data
|
||||
* @return $this
|
||||
*/
|
||||
public function setData($data): self
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current validator.
|
||||
*
|
||||
* @param Validator $validator
|
||||
* @return $this
|
||||
*/
|
||||
public function setValidator($validator): self
|
||||
{
|
||||
/** @var UploadedFile|null $file */
|
||||
$file = Arr::get($validator->getData(), 'video');
|
||||
|
||||
$ffprobeData = Arr::get($validator->getData(), 'ffprobeData');
|
||||
if ($ffprobeData === null && $file !== null) {
|
||||
$data = array_merge($validator->getData(), ['ffprobeData' => $this->getFFprobeData($file)]);
|
||||
|
||||
$validator->setData($data);
|
||||
}
|
||||
|
||||
$loudnessStats = Arr::get($validator->getData(), 'loudnessStats');
|
||||
if ($loudnessStats === null && $file !== null) {
|
||||
$data = array_merge($validator->getData(), ['loudnessStats' => $this->getLoudnessStats($file)]);
|
||||
|
||||
$validator->setData($data);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the FFprobe data for the uploaded file.
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @return array
|
||||
*/
|
||||
protected function getFFprobeData(UploadedFile $file): array
|
||||
{
|
||||
$commands = [
|
||||
$file->path(),
|
||||
'-v',
|
||||
'quiet',
|
||||
'-print_format',
|
||||
'json',
|
||||
'-show_streams',
|
||||
'-show_format',
|
||||
'-show_chapters',
|
||||
];
|
||||
|
||||
$output = FFProbe::create()
|
||||
->getFFProbeDriver()
|
||||
->command($commands);
|
||||
|
||||
return json_decode($output, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the loudness stats for the uploaded file.
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @return array
|
||||
*/
|
||||
protected function getLoudnessStats(UploadedFile $file): array
|
||||
{
|
||||
$filter = [
|
||||
'-hide_banner',
|
||||
'-nostats',
|
||||
'-vn',
|
||||
'-sn',
|
||||
'-dn',
|
||||
'-filter:a',
|
||||
'loudnorm=I=-16:LRA=20:TP=-1:dual_mono=true:linear=true:print_format=json',
|
||||
'-f',
|
||||
'null',
|
||||
];
|
||||
|
||||
$output = FFMpeg::open($file)
|
||||
->export()
|
||||
->addFilter($filter)
|
||||
->getProcessOutput();
|
||||
|
||||
$output = Arr::join($output->all(), '');
|
||||
$loudness = Str::match('/{[^}]*}/m', $output);
|
||||
|
||||
return json_decode($loudness, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get submission streams.
|
||||
*
|
||||
* @return StreamCollection
|
||||
*/
|
||||
protected function streams(): StreamCollection
|
||||
{
|
||||
$ffprobeData = Arr::get($this->data, 'ffprobeData');
|
||||
|
||||
$mapper = new Mapper();
|
||||
|
||||
return $mapper->map('streams', $ffprobeData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get submission format.
|
||||
*
|
||||
* @return Format
|
||||
*/
|
||||
protected function format(): Format
|
||||
{
|
||||
$ffprobeData = Arr::get($this->data, 'ffprobeData');
|
||||
|
||||
$mapper = new Mapper();
|
||||
|
||||
return $mapper->map('format', $ffprobeData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get submission chapters.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function chapters(): array
|
||||
{
|
||||
return Arr::get($this->data, 'ffprobeData.chapters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the submission loudness stats.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function loudness(): array
|
||||
{
|
||||
return Arr::get($this->data, 'loudnessStats');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Video;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class VideoCodecStreamRule.
|
||||
*/
|
||||
class VideoCodecStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$video = $this->streams()
|
||||
->videos()
|
||||
->first();
|
||||
|
||||
return $video !== null && $video->get('codec_name') === 'vp9';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.video_codec');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Video;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class VideoColorPrimariesStreamRule.
|
||||
*/
|
||||
class VideoColorPrimariesStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$video = $this->streams()
|
||||
->videos()
|
||||
->first();
|
||||
|
||||
$colorPrimaries = ['bt709', 'smpte170m', 'bt470bg'];
|
||||
|
||||
return $video !== null && in_array($video->get('color_primaries'), $colorPrimaries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.video_color_primaries');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Video;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class VideoColorSpaceStreamRule.
|
||||
*/
|
||||
class VideoColorSpaceStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$video = $this->streams()
|
||||
->videos()
|
||||
->first();
|
||||
|
||||
$colorSpaces = ['bt709', 'smpte170m', 'bt470bg'];
|
||||
|
||||
return $video !== null && in_array($video->get('color_space'), $colorSpaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.video_color_space');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Video;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class VideoColorTransferStreamRule.
|
||||
*/
|
||||
class VideoColorTransferStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$video = $this->streams()
|
||||
->videos()
|
||||
->first();
|
||||
|
||||
$colorTransfers = ['bt709', 'smpte170m', 'bt470bg'];
|
||||
|
||||
return $video !== null && in_array($video->get('color_transfer'), $colorTransfers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.video_color_transfer');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Video;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use FFMpeg\FFProbe\DataMapping\Stream;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class VideoIndexStreamRule.
|
||||
*/
|
||||
class VideoIndexStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$streams = $this->streams()->all();
|
||||
|
||||
return collect($streams)->contains(fn (Stream $stream) => $stream->isVideo() && $stream->get('index') === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.video_index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Rules\Wiki\Submission\Video;
|
||||
|
||||
use App\Rules\Wiki\Submission\SubmissionRule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class VideoPixelFormatStreamRule.
|
||||
*/
|
||||
class VideoPixelFormatStreamRule extends SubmissionRule
|
||||
{
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param UploadedFile $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$video = $this->streams()
|
||||
->videos()
|
||||
->first();
|
||||
|
||||
return $video !== null && $video->get('pix_fmt') === 'yuv420p';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function message(): string|array
|
||||
{
|
||||
return __('validation.submission.video_pixel_format');
|
||||
}
|
||||
}
|
||||
+22
-9
@@ -68,16 +68,29 @@ return [
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'videos' => [
|
||||
'videos_nyc' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('VIDEO_ACCESS_KEY_ID'),
|
||||
'secret' => env('VIDEO_SECRET_ACCESS_KEY'),
|
||||
'region' => env('VIDEO_DEFAULT_REGION'),
|
||||
'bucket' => env('VIDEO_BUCKET'),
|
||||
'endpoint' => env('VIDEO_ENDPOINT'),
|
||||
'stream_reads' => env('VIDEO_STREAM_READS'),
|
||||
'disable_asserts' => env('VIDEO_DISABLE_ASSERTS'),
|
||||
'visibility' => env('VIDEO_VISIBILITY'),
|
||||
'key' => env('VIDEO_NYC_ACCESS_KEY_ID'),
|
||||
'secret' => env('VIDEO_NYC_SECRET_ACCESS_KEY'),
|
||||
'region' => env('VIDEO_NYC_DEFAULT_REGION'),
|
||||
'bucket' => env('VIDEO_NYC_BUCKET'),
|
||||
'endpoint' => env('VIDEO_NYC_ENDPOINT'),
|
||||
'stream_reads' => env('VIDEO_NYC_STREAM_READS'),
|
||||
'disable_asserts' => env('VIDEO_NYC_DISABLE_ASSERTS'),
|
||||
'visibility' => env('VIDEO_NYC_VISIBILITY'),
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'videos_fra' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('VIDEO_FRA_ACCESS_KEY_ID'),
|
||||
'secret' => env('VIDEO_FRA_SECRET_ACCESS_KEY'),
|
||||
'region' => env('VIDEO_FRA_DEFAULT_REGION'),
|
||||
'bucket' => env('VIDEO_FRA_BUCKET'),
|
||||
'endpoint' => env('VIDEO_FRA_ENDPOINT'),
|
||||
'stream_reads' => env('VIDEO_FRA_STREAM_READS'),
|
||||
'disable_asserts' => env('VIDEO_FRA_DISABLE_ASSERTS'),
|
||||
'visibility' => env('VIDEO_FRA_VISIBILITY'),
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
|
||||
@@ -49,4 +49,20 @@ return [
|
||||
'streaming_method' => env('VIDEO_STREAMING_METHOD', 'response'),
|
||||
|
||||
'nginx_redirect' => env('VIDEO_NGINX_REDIRECT', '/video_redirect/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Video Uploading
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These values facilitate the validation and uploading of video in-system to object storage.
|
||||
| For validation, we want to enforce that the latest FFmpeg version is used.
|
||||
| We will use the libavformat version of the form "Lavf{major}.{minor}.{patch]".
|
||||
| For uploading, we want to specify the target disks to upload the file.
|
||||
|
|
||||
*/
|
||||
|
||||
'encoder_version' => env('VIDEO_ENCODER_VERSION'),
|
||||
|
||||
'upload_disks' => explode(',', env('VIDEO_UPLOAD_DISKS')),
|
||||
];
|
||||
|
||||
@@ -21,6 +21,10 @@ parameters:
|
||||
- '#Call to an undefined method Mockery\\ExpectationInterface|Mockery\\HigherOrderMessage::once\(\).#'
|
||||
- '#Strict comparison using === between class-string<Laravel\\Nova\\Resource> and null will always evaluate to false.#'
|
||||
- '#Call to an undefined method ProtoneMedia\\LaravelFFMpeg\\Drivers\\PHPFFMpeg::export\(\).#'
|
||||
- '#Call to an undefined method ProtoneMedia\\LaravelFFMpeg\\Drivers\\PHPFFMpeg::getProcessOutput\(\).#'
|
||||
-
|
||||
message: '#Unreachable statement - code above always terminates.#'
|
||||
path: app/Http/Middleware/ThrottleRequestsWithService.php
|
||||
-
|
||||
message: '#Method App\\Rules\\Wiki\\Submission\\SubmissionRule::#'
|
||||
path: app/Rules/Wiki/Submission/SubmissionRule.php
|
||||
|
||||
@@ -228,6 +228,10 @@ return [
|
||||
'updated_at_end' => 'Updated At End',
|
||||
'updated_at_start' => 'Updated At Start',
|
||||
'updated_at' => 'Updated At',
|
||||
'upload_video_help' => 'The WebM to upload. WebMs will be uploaded to each configured storage disk.',
|
||||
'upload_video_path_help' => 'The directory the video will be uploaded to. Ex: 2022/Spring.',
|
||||
'upload_video' => 'Upload Video',
|
||||
'upload' => 'Upload',
|
||||
'usage' => 'Usage',
|
||||
'user' => 'User',
|
||||
'users' => 'Users',
|
||||
|
||||
@@ -135,6 +135,29 @@ return [
|
||||
],
|
||||
'starts_with' => 'The :attribute must start with one of the following: :values.',
|
||||
'string' => 'The :attribute must be a string.',
|
||||
'submission' => [
|
||||
'audio_channel_layout' => 'Incorrect audio layout.',
|
||||
'audio_channels' => 'Incorrect audio channels.',
|
||||
'audio_codec' => 'Incorrect audio codec.',
|
||||
'audio_index' => 'Second stream is not audio.',
|
||||
'audio_loudness_integrated_target' => 'Unexpected target loudness.',
|
||||
'audio_loudness_true_peak' => 'Unexpected true peak.',
|
||||
'audio_sample_rate' => 'Incorrect sample rate.',
|
||||
'file_name' => 'File name is not expected pattern of \'[Title]-{OP|ED}#v#-[Tags]\'',
|
||||
'format_bitrate_restriction' => 'File size restriction violated.',
|
||||
'format_encoder_name' => 'Incorrect encoder.',
|
||||
'format_encoder_version' => 'FFmpeg build is out of date.',
|
||||
'format_extraneous_chapters' => 'Extraneous menu data.',
|
||||
'format_extraneous_metadata' => 'Extraneous source file metadata.',
|
||||
'format_format_name' => 'Incorrect file format.',
|
||||
'format_total_streams' => 'The file must have exactly two streams.',
|
||||
'video_codec' => 'Incorrect video codec.',
|
||||
'video_color_primaries' => 'Unexpected color primaries.',
|
||||
'video_color_space' => 'Unexpected color space.',
|
||||
'video_color_transfer' => 'Unexpected color transfer.',
|
||||
'video_index' => 'First stream is not video.',
|
||||
'video_pixel_format' => 'Incorrect pixel format.',
|
||||
],
|
||||
'timezone' => 'The :attribute must be a valid timezone.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'uploaded' => 'The :attribute failed to upload.',
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Actions\Models\Wiki\Video;
|
||||
|
||||
use App\Actions\Models\Wiki\Video\UploadVideoAction;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Illuminate\Http\Testing\File;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Class UploadVideoTest.
|
||||
*/
|
||||
class UploadVideoTest extends TestCase
|
||||
{
|
||||
use WithFaker;
|
||||
|
||||
/**
|
||||
* The Upload Video Action shall fail if there are no uploads.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testDefault(): void
|
||||
{
|
||||
Config::set('video.upload_disks', []);
|
||||
Storage::fake(Config::get('video.disk'));
|
||||
|
||||
$file = File::fake()->create($this->faker->word().'.webm', $this->faker->randomDigitNotNull());
|
||||
|
||||
$action = new UploadVideoAction($file, $this->faker->word());
|
||||
|
||||
$result = $action->handle();
|
||||
|
||||
static::assertTrue($result->hasFailed());
|
||||
}
|
||||
|
||||
/**
|
||||
* The Upload Video Action shall pass if given a valid file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testPassed(): void
|
||||
{
|
||||
Storage::fake(Config::get('video.disk'));
|
||||
Config::set('video.upload_disks', [Config::get('video.disk')]);
|
||||
|
||||
$file = File::fake()->create($this->faker->word().'.webm', $this->faker->randomDigitNotNull());
|
||||
|
||||
$action = new UploadVideoAction($file, $this->faker->word());
|
||||
|
||||
$result = $action->handle();
|
||||
|
||||
static::assertTrue(ActionStatus::PASSED()->is($result->getStatus()));
|
||||
}
|
||||
|
||||
/**
|
||||
* The Upload Video Action shall upload the file to the configured disk.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testUploadedToDisk(): void
|
||||
{
|
||||
Storage::fake(Config::get('video.disk'));
|
||||
Config::set('video.upload_disks', [Config::get('video.disk')]);
|
||||
|
||||
$file = File::fake()->create($this->faker->word().'.webm', $this->faker->randomDigitNotNull());
|
||||
|
||||
$action = new UploadVideoAction($file, $this->faker->word());
|
||||
|
||||
$action->handle();
|
||||
|
||||
static::assertCount(1, Storage::disk(Config::get('video.disk'))->allFiles());
|
||||
}
|
||||
|
||||
/**
|
||||
* The Upload Video Action shall upload the file to the configured disk.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatedVideo(): void
|
||||
{
|
||||
Storage::fake(Config::get('video.disk'));
|
||||
Config::set('video.upload_disks', [Config::get('video.disk')]);
|
||||
|
||||
$file = File::fake()->create($this->faker->word().'.webm', $this->faker->randomDigitNotNull());
|
||||
|
||||
$action = new UploadVideoAction($file, $this->faker->word());
|
||||
|
||||
$action->handle();
|
||||
|
||||
static::assertDatabaseCount(Video::class, 1);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ use App\Repositories\Storage\Wiki\VideoRepository;
|
||||
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;
|
||||
|
||||
@@ -28,6 +30,8 @@ class VideoReconcileTest extends TestCase
|
||||
*/
|
||||
public function testNoResults(): void
|
||||
{
|
||||
Storage::fake(Config::get('video.disk'));
|
||||
|
||||
$this->baseRefreshDatabase(); // Cannot lazily refresh database within pending command
|
||||
|
||||
$this->mock(VideoRepository::class, function (MockInterface $mock) {
|
||||
@@ -44,6 +48,8 @@ class VideoReconcileTest extends TestCase
|
||||
*/
|
||||
public function testCreated(): void
|
||||
{
|
||||
Storage::fake(Config::get('video.disk'));
|
||||
|
||||
$this->baseRefreshDatabase(); // Cannot lazily refresh database within pending command
|
||||
|
||||
$createdVideoCount = $this->faker->numberBetween(2, 9);
|
||||
@@ -64,6 +70,8 @@ class VideoReconcileTest extends TestCase
|
||||
*/
|
||||
public function testDeleted(): void
|
||||
{
|
||||
Storage::fake(Config::get('video.disk'));
|
||||
|
||||
$deletedVideoCount = $this->faker->numberBetween(2, 9);
|
||||
|
||||
Video::factory()->count($deletedVideoCount)->create();
|
||||
@@ -82,6 +90,8 @@ class VideoReconcileTest extends TestCase
|
||||
*/
|
||||
public function testUpdated(): void
|
||||
{
|
||||
Storage::fake(Config::get('video.disk'));
|
||||
|
||||
$updatedVideoCount = $this->faker->numberBetween(2, 9);
|
||||
|
||||
$basenames = collect($this->faker->words($updatedVideoCount));
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -116,6 +117,8 @@ class VideoTest extends TestCase
|
||||
*/
|
||||
public function testStreamedThroughResponse(): void
|
||||
{
|
||||
Storage::fake(Config::get('video.disk'));
|
||||
|
||||
Config::set(FlagConstants::ALLOW_VIDEO_STREAMS_FLAG_QUALIFIED, true);
|
||||
Config::set('video.streaming_method', 'response');
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Actions\Models\Wiki\Video;
|
||||
|
||||
use App\Actions\Models\Wiki\Video\UploadResults;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Class UploadResultsTest.
|
||||
*/
|
||||
class UploadResultsTest extends TestCase
|
||||
{
|
||||
use WithFaker;
|
||||
|
||||
/**
|
||||
* The Action result has failed if there are no uploads.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testDefault(): void
|
||||
{
|
||||
$uploadResults = new UploadResults();
|
||||
|
||||
$result = $uploadResults->toActionResult();
|
||||
|
||||
static::assertTrue($result->hasFailed());
|
||||
}
|
||||
|
||||
/**
|
||||
* The Action result has failed if any uploads have returned false.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testFailed(): void
|
||||
{
|
||||
$uploads = [];
|
||||
|
||||
foreach (range(0, $this->faker->randomDigitNotNull()) as $ignored) {
|
||||
$uploads[$this->faker->word()] = $this->faker->filePath();
|
||||
}
|
||||
|
||||
foreach (range(0, $this->faker->randomDigitNotNull()) as $ignored) {
|
||||
$uploads[$this->faker->word()] = false;
|
||||
}
|
||||
|
||||
$uploadResults = new UploadResults($uploads);
|
||||
|
||||
$result = $uploadResults->toActionResult();
|
||||
|
||||
static::assertTrue($result->hasFailed());
|
||||
}
|
||||
|
||||
/**
|
||||
* The Action result has passed if all uploads have returned the file path.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testPassed(): void
|
||||
{
|
||||
$uploads = [];
|
||||
|
||||
foreach (range(0, $this->faker->randomDigitNotNull()) as $ignored) {
|
||||
$uploads[$this->faker->word()] = $this->faker->filePath();
|
||||
}
|
||||
|
||||
$uploadResults = new UploadResults($uploads);
|
||||
|
||||
$result = $uploadResults->toActionResult();
|
||||
|
||||
static::assertTrue(ActionStatus::PASSED()->is($result->getStatus()));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Unit\Rules\Wiki;
|
||||
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Rules\Wiki\ResourceLinkMatchesSiteRule;
|
||||
use App\Rules\Wiki\Resource\ResourceLinkMatchesSiteRule;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Unit\Rules\Wiki;
|
||||
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Rules\Wiki\ResourceSiteMatchesLinkRule;
|
||||
use App\Rules\Wiki\Resource\ResourceSiteMatchesLinkRule;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user