first commit
This commit is contained in:
19
app/Helpers/FirstParty/Jobs/JobTrigger.php
Normal file
19
app/Helpers/FirstParty/Jobs/JobTrigger.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\RunVideoRenderPipelineJob;
|
||||
use App\Models\Video;
|
||||
|
||||
class JobTrigger
|
||||
{
|
||||
public static function RunVideoRenderPipelineJob()
|
||||
{
|
||||
$video = Video::latest()->first();
|
||||
|
||||
if ($video) {
|
||||
$job = new RunVideoRenderPipelineJob($video->id);
|
||||
$job->handle();
|
||||
} else {
|
||||
echo 'NO VIDEO';
|
||||
}
|
||||
}
|
||||
}
|
||||
349
app/Helpers/FirstParty/MediaEngine/MediaEngine.php
Normal file
349
app/Helpers/FirstParty/MediaEngine/MediaEngine.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\MediaEngine;
|
||||
|
||||
use App\Models\Media;
|
||||
use App\Models\MediaCollection;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MediaEngine
|
||||
{
|
||||
const LOCAL_TEMP_DISK = 'local_temp';
|
||||
|
||||
const USER_UPLOADED = 'user_uploaded';
|
||||
|
||||
const USER_RENDERED = 'user_rendered';
|
||||
|
||||
const USER = 'user';
|
||||
|
||||
public static function getMediaCloudUrl($media)
|
||||
{
|
||||
return Storage::disk($media->disk)->url($media->file_path.$media->file_name);
|
||||
}
|
||||
|
||||
public static function loadMediaToLocalTemp($uuid)
|
||||
{
|
||||
$media = self::getMediaByUuid($uuid);
|
||||
|
||||
$result = [
|
||||
'media' => $media,
|
||||
'uuid' => $uuid,
|
||||
'temp_path' => null,
|
||||
'temp' => null,
|
||||
];
|
||||
|
||||
if (! $media) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (! Storage::disk($media->disk)->exists($media->file_path.$media->file_name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$tempPath = 'temp_'.$media->file_name;
|
||||
$fileContent = Storage::disk($media->disk)->get($media->file_path.$media->file_name);
|
||||
|
||||
if (! Storage::disk(self::LOCAL_TEMP_DISK)->exists($tempPath)) {
|
||||
if (Storage::disk(self::LOCAL_TEMP_DISK)->put($tempPath, $fileContent)) {
|
||||
$result['temp_path'] = $tempPath;
|
||||
$result['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
|
||||
}
|
||||
} else {
|
||||
$result['temp_path'] = $tempPath;
|
||||
$result['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function loadMediasToLocalTemp(array $uuids)
|
||||
{
|
||||
$medias = self::getMediaByUuids($uuids);
|
||||
$result = [];
|
||||
|
||||
foreach ($medias as $media) {
|
||||
if (! $media) {
|
||||
continue; // Skip invalid media instead of returning early
|
||||
}
|
||||
|
||||
$tempPath = 'temp_'.$media->file_name;
|
||||
$singleResult = [
|
||||
'media' => $media,
|
||||
'uuid' => $media->uuid,
|
||||
'temp_path' => null,
|
||||
'temp' => null,
|
||||
'cloud_url' => Storage::disk($media->disk)->url($media->file_path.$media->file_name),
|
||||
];
|
||||
|
||||
// Check if file exists in cloud storage
|
||||
if (! Storage::disk($media->disk)->exists($media->file_path.$media->file_name)) {
|
||||
$result[] = $singleResult; // Add to results but with null temp paths
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already loaded in local temp
|
||||
if (Storage::disk(self::LOCAL_TEMP_DISK)->exists($tempPath)) {
|
||||
// File already exists in temp, just set the paths
|
||||
$singleResult['temp_path'] = $tempPath;
|
||||
$singleResult['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
|
||||
} else {
|
||||
// File needs to be loaded to temp
|
||||
$fileContent = Storage::disk($media->disk)->get($media->file_path.$media->file_name);
|
||||
if (Storage::disk(self::LOCAL_TEMP_DISK)->put($tempPath, $fileContent)) {
|
||||
$singleResult['temp_path'] = $tempPath;
|
||||
$singleResult['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = $singleResult;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function deleteMediaFromLocalTemp($uuid)
|
||||
{
|
||||
$result = self::loadMediaToLocalTemp($uuid);
|
||||
|
||||
if ($result['temp_path']) {
|
||||
Storage::disk(self::LOCAL_TEMP_DISK)->delete($result['temp_path']);
|
||||
}
|
||||
|
||||
return [
|
||||
'media' => $result['media'],
|
||||
'uuid' => $uuid,
|
||||
'temp_path' => null,
|
||||
'temp' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public static function deleteMediasFromLocalTemp(array $uuids)
|
||||
{
|
||||
$results = self::loadMediasToLocalTemp($uuids);
|
||||
$deletedResults = [];
|
||||
|
||||
foreach ($results as $result) {
|
||||
if ($result['temp_path']) {
|
||||
Storage::disk(self::LOCAL_TEMP_DISK)->delete($result['temp_path']);
|
||||
}
|
||||
|
||||
$deletedResults[] = [
|
||||
'media' => $result['media'],
|
||||
'uuid' => $result['uuid'],
|
||||
'temp_path' => null,
|
||||
'temp' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return $deletedResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new media file to the specified media collection.
|
||||
*
|
||||
* Example: $newMedia = MediaEngine::addMedia(
|
||||
* 'bgm_collection',
|
||||
* 'bgm',
|
||||
* 'user_upload',
|
||||
* 'web_app',
|
||||
* 'background_music.mp3',
|
||||
* $fileContent,
|
||||
* 'r2',
|
||||
* 'name of file',
|
||||
* $user_id,
|
||||
* );
|
||||
*/
|
||||
public static function addMedia(
|
||||
string $mediaCollectionKey,
|
||||
string $mediaType,
|
||||
string $mediaSource,
|
||||
string $mediaProvider,
|
||||
string $fileName,
|
||||
string $fileContent,
|
||||
string $disk = 'r2',
|
||||
?string $name = null,
|
||||
?int $userId = null,
|
||||
|
||||
) {
|
||||
$mediaCollection = MediaCollection::where('key', $mediaCollectionKey)->first();
|
||||
|
||||
if (! $mediaCollection) {
|
||||
throw new \InvalidArgumentException("Media collection with key '{$mediaCollectionKey}' not found.");
|
||||
}
|
||||
|
||||
$config = config("platform.media.{$mediaCollectionKey}");
|
||||
|
||||
// Adjust fileName with prefix, postfix, and ensure extension
|
||||
$adjustedFileName = $config['prefix'].epoch_now_timestamp().'-'.$fileName;
|
||||
|
||||
// Construct file path
|
||||
$filePath = $config['location'];
|
||||
|
||||
// Store the file
|
||||
$stored = Storage::disk($disk)->put($filePath.$adjustedFileName, $fileContent);
|
||||
|
||||
if (! $stored) {
|
||||
throw new \RuntimeException("Failed to store file: {$filePath}");
|
||||
}
|
||||
|
||||
// Get filetype
|
||||
$mimeType = null;
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'mime_');
|
||||
try {
|
||||
file_put_contents($tempFile, $fileContent);
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $tempFile);
|
||||
finfo_close($finfo);
|
||||
} finally {
|
||||
// This ensures the file is deleted even if an exception occurs
|
||||
if (file_exists($tempFile)) {
|
||||
unlink($tempFile);
|
||||
}
|
||||
}
|
||||
if (is_empty($mimeType)) {
|
||||
$mimeType = $config['mime'];
|
||||
}
|
||||
|
||||
$media = new Media([
|
||||
'uuid' => Str::uuid(),
|
||||
'media_collection_id' => $mediaCollection->id,
|
||||
'user_id' => $userId,
|
||||
'media_type' => $mediaType,
|
||||
'media_source' => $mediaSource,
|
||||
'media_provider' => $mediaProvider,
|
||||
'mime_type' => $mimeType,
|
||||
'file_name' => $adjustedFileName,
|
||||
'file_path' => $filePath,
|
||||
'disk' => $disk,
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
$media->save();
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
private static function ensureUniqueFileName($fileName, $disk, $location)
|
||||
{
|
||||
$uniqueFileName = $fileName;
|
||||
$counter = 1;
|
||||
|
||||
while (Storage::disk($disk)->exists($location.$uniqueFileName)) {
|
||||
$info = pathinfo($fileName);
|
||||
$uniqueFileName = $info['filename'].'-'.$counter.'.'.$info['extension'];
|
||||
$counter++;
|
||||
}
|
||||
|
||||
return $uniqueFileName;
|
||||
}
|
||||
|
||||
public static function getMediaByUuid($uuid)
|
||||
{
|
||||
return Media::where('uuid', $uuid)->first();
|
||||
}
|
||||
|
||||
public static function getMediaByUuids(array $uuids)
|
||||
{
|
||||
return Media::whereIn('uuid', $uuids)->get();
|
||||
}
|
||||
|
||||
public static function getMediasByCollectionKey($key)
|
||||
{
|
||||
$collection = MediaCollection::where('key', $key)->first();
|
||||
|
||||
if (! $collection) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $collection->media;
|
||||
}
|
||||
|
||||
public static function getCollectionKeyByOwnerMediaType($owner_type, $media_type)
|
||||
{
|
||||
$mediaConfig = config('platform.media');
|
||||
|
||||
foreach ($mediaConfig as $key => $item) {
|
||||
if ($item['owner_type'] == $owner_type && $item['media_type'] == $media_type) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file details from a URL including filename, extension and MIME type
|
||||
*
|
||||
* @param string $url The URL of the file
|
||||
* @return object Object containing filename, extension and MIME type
|
||||
*/
|
||||
public static function getFileDetailsbyUrl($url)
|
||||
{
|
||||
// Create an empty result object
|
||||
$result = new \stdClass;
|
||||
|
||||
// Parse the URL to extract the filename
|
||||
$pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
|
||||
|
||||
// Set the filename and extension
|
||||
$result->filename = $pathInfo['filename'] ?? '';
|
||||
$result->extension = $pathInfo['extension'] ?? '';
|
||||
|
||||
// Initialize the MIME type as unknown
|
||||
$result->mimetype = 'application/octet-stream';
|
||||
|
||||
// Try to get the real MIME type using fileinfo
|
||||
try {
|
||||
// Create a temporary file
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'file_');
|
||||
|
||||
// Get the file content from URL
|
||||
$fileContent = @file_get_contents($url);
|
||||
|
||||
if ($fileContent !== false) {
|
||||
// Write the content to the temporary file
|
||||
file_put_contents($tempFile, $fileContent);
|
||||
|
||||
// Create a finfo object
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
|
||||
// Get the MIME type
|
||||
$result->mimetype = $finfo->file($tempFile);
|
||||
|
||||
// Clean up the temporary file
|
||||
@unlink($tempFile);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If there's an error, try to determine MIME type from extension
|
||||
$commonMimeTypes = [
|
||||
// Images
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'svg' => 'image/svg+xml',
|
||||
|
||||
// Audio
|
||||
'mp3' => 'audio/mpeg',
|
||||
'wav' => 'audio/wav',
|
||||
'ogg' => 'audio/ogg',
|
||||
'm4a' => 'audio/mp4',
|
||||
'flac' => 'audio/flac',
|
||||
|
||||
// Video
|
||||
'mp4' => 'video/mp4',
|
||||
'webm' => 'video/webm',
|
||||
'avi' => 'video/x-msvideo',
|
||||
'mov' => 'video/quicktime',
|
||||
'mkv' => 'video/x-matroska',
|
||||
];
|
||||
|
||||
if (! empty($result->extension) && isset($commonMimeTypes[strtolower($result->extension)])) {
|
||||
$result->mimetype = $commonMimeTypes[strtolower($result->extension)];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
163
app/Helpers/FirstParty/Render/FfmpegVideoRenderer.php
Normal file
163
app/Helpers/FirstParty/Render/FfmpegVideoRenderer.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\Render;
|
||||
|
||||
use App\Models\Video;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* Class FfmpegVideoRenderer
|
||||
*
|
||||
* Builds and optionally executes an ffmpeg command using VideoElement models and render settings.
|
||||
*/
|
||||
class FfmpegVideoRenderer
|
||||
{
|
||||
/**
|
||||
* Render the given Video to a file via ffmpeg.
|
||||
*
|
||||
* @return object { string $name, string $path }
|
||||
*
|
||||
* @throws \InvalidArgumentException if required elements missing
|
||||
* @throws \RuntimeException on ffmpeg failure
|
||||
*/
|
||||
public static function render(Video $video): object
|
||||
{
|
||||
$elements = self::get_video_elements($video);
|
||||
$settings = self::get_render_settings($video);
|
||||
|
||||
// Gather elements by type and sort by time
|
||||
$slide_images = $elements->where('external_reference', 'slideshow')->sortBy('time')->values();
|
||||
$overlay_images = $elements->where('type', 'image')
|
||||
->where('external_reference', '!=', 'slideshow')
|
||||
->sortBy('time')
|
||||
->values();
|
||||
$video_element = $elements->firstWhere('type', 'video');
|
||||
$audio_tracks = $elements->where('type', 'audio')->sortBy('time')->values();
|
||||
|
||||
if ($slide_images->isEmpty() || ! $video_element) {
|
||||
throw new \InvalidArgumentException('At least one slideshow and one video element are required.');
|
||||
}
|
||||
|
||||
// Build ffmpeg input arguments
|
||||
$input_args = [];
|
||||
foreach ($slide_images as $slide) {
|
||||
$duration = number_format($slide->duration, 2);
|
||||
$input_args[] = "-loop 1 -t {$duration} -i \"{$slide->asset_url}\"";
|
||||
}
|
||||
foreach ($overlay_images as $overlay) {
|
||||
$duration = number_format($overlay->duration, 2);
|
||||
$input_args[] = "-loop 1 -t {$duration} -i \"{$overlay->asset_url}\"";
|
||||
}
|
||||
$input_args[] = "-i \"{$video_element->asset_url}\"";
|
||||
foreach ($audio_tracks as $audio) {
|
||||
$input_args[] = "-i \"{$audio->asset_url}\"";
|
||||
}
|
||||
|
||||
// Build filter_complex chains
|
||||
$filters = [];
|
||||
$slide_count = $slide_images->count();
|
||||
$concat_input = '';
|
||||
for ($i = 0; $i < $slide_count; $i++) {
|
||||
$concat_input .= "[{$i}:v]";
|
||||
}
|
||||
$filters[] = "{$concat_input}concat=n={$slide_count}:v=1:a=0[slideshow]";
|
||||
|
||||
$current_label = 'slideshow';
|
||||
foreach ($overlay_images as $idx => $overlay) {
|
||||
$stream_idx = $slide_count + $idx;
|
||||
$start_time = number_format($overlay->time, 2);
|
||||
$end_time = number_format($overlay->time + $overlay->duration, 2);
|
||||
$next_label = "overlay{$idx}";
|
||||
$filters[] = "[{$current_label}][{$stream_idx}:v]overlay=enable=between(t\,{$start_time}\,{$end_time})[{$next_label}]";
|
||||
$current_label = $next_label;
|
||||
}
|
||||
|
||||
$video_idx = $slide_count + $overlay_images->count();
|
||||
$v_start = number_format($video_element->time, 2);
|
||||
$v_end = number_format($video_element->time + $video_element->duration, 2);
|
||||
$filters[] = "[{$current_label}][{$video_idx}:v]overlay=enable=between(t\,{$v_start}\,{$v_end})[v_out]";
|
||||
|
||||
$audio_labels = [];
|
||||
$first_audio = $video_idx + 1;
|
||||
foreach ($audio_tracks as $idx => $audio) {
|
||||
$stream_idx = $first_audio + $idx;
|
||||
$dur = number_format($audio->duration, 2);
|
||||
$start_time = number_format($audio->time, 2);
|
||||
|
||||
$chain = "[{$stream_idx}:a]atrim=duration={$dur},asetpts=PTS-STARTPTS";
|
||||
if ($audio->time > 0) {
|
||||
$delay_ms = (int) round($audio->time * 1000);
|
||||
$chain .= ",adelay={$delay_ms}|{$delay_ms}";
|
||||
}
|
||||
|
||||
$label = "a{$idx}";
|
||||
$filters[] = "{$chain}[{$label}]";
|
||||
$audio_labels[] = $label;
|
||||
}
|
||||
|
||||
$mix_input = '';
|
||||
foreach ($audio_labels as $lbl) {
|
||||
$mix_input .= "[{$lbl}]";
|
||||
}
|
||||
$audio_count = count($audio_labels);
|
||||
$filters[] = "{$mix_input}amix=inputs={$audio_count}:duration=longest[a_out]";
|
||||
|
||||
$filter_complex = implode(';', $filters);
|
||||
|
||||
// Prepare output
|
||||
$filename = Str::random(12).'.mp4';
|
||||
$output_path = sys_get_temp_dir().DIRECTORY_SEPARATOR.$filename;
|
||||
|
||||
// FFmpeg command, including render settings
|
||||
$video_bitrate = $settings['video_bitrate'];
|
||||
$audio_bitrate = $settings['audio_bitrate'];
|
||||
$fps = $settings['fps'];
|
||||
|
||||
$command = 'ffmpeg '.implode(' ', $input_args)
|
||||
." -r {$fps}"
|
||||
." -filter_complex \"{$filter_complex}\""
|
||||
.' -map "[v_out]" -map "[a_out]"'
|
||||
." -c:v libx264 -b:v {$video_bitrate}"
|
||||
." -c:a aac -b:a {$audio_bitrate}"
|
||||
." -shortest \"{$output_path}\" -y";
|
||||
|
||||
$process = Process::fromShellCommandline($command, null, null, null, 300);
|
||||
$process->run();
|
||||
|
||||
if (! $process->isSuccessful()) {
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'exception' => new \RuntimeException('FFmpeg failed: '.$process->getErrorOutput()),
|
||||
];
|
||||
throw new \RuntimeException('FFmpeg failed: '.$process->getErrorOutput());
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'name' => $filename,
|
||||
'path' => $output_path,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch video elements for rendering.
|
||||
*/
|
||||
private static function get_video_elements(Video $video): Collection
|
||||
{
|
||||
return $video->video_elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract render settings or defaults.
|
||||
*/
|
||||
private static function get_render_settings(Video $video): array
|
||||
{
|
||||
return [
|
||||
'video_bitrate' => $video->render_settings?->video_bitrate ?? '3M',
|
||||
'audio_bitrate' => $video->render_settings?->audio_bitrate ?? '128k',
|
||||
'fps' => $video->render_settings?->fps ?? '30',
|
||||
];
|
||||
}
|
||||
}
|
||||
27
app/Helpers/FirstParty/Render/RenderConstants.php
Normal file
27
app/Helpers/FirstParty/Render/RenderConstants.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\Render;
|
||||
|
||||
class RenderConstants
|
||||
{
|
||||
const STATUS_PLANNED = 'STATUS_PLANNED'; // the render is queued for rendering
|
||||
|
||||
const STATUS_WAITING = 'STATUS_WAITING'; // the render is waiting for a third-party service (e.g., OpenAI or ElevenLabs) to finish
|
||||
|
||||
const STATUS_TRANSCRIBING = 'STATUS_TRANSCRIBING'; // an input file is being transcribed
|
||||
|
||||
const STATUS_RENDERING = 'STATUS_RENDERING'; // the render is being processed
|
||||
|
||||
const STATUS_SUCCEEDED = 'STATUS_SUCCEEDED'; // the render has been completed successfully
|
||||
|
||||
const STATUS_FAILED = 'STATUS_FAILED'; // the render failed due to the reason specified in the error_message field
|
||||
|
||||
const STATUSES = [
|
||||
self::STATUS_PLANNED,
|
||||
self::STATUS_WAITING,
|
||||
self::STATUS_TRANSCRIBING,
|
||||
self::STATUS_RENDERING,
|
||||
self::STATUS_SUCCEEDED,
|
||||
self::STATUS_FAILED,
|
||||
];
|
||||
}
|
||||
71
app/Helpers/Global/cast_helpers.php
Normal file
71
app/Helpers/Global/cast_helpers.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('array_to_object_2025')) {
|
||||
/**
|
||||
* A magic function that turns all nested array into nested objects. Calling it with no parameter would result in a empty object.
|
||||
*
|
||||
* @param mixed $array The array to be recursively casted into an object.
|
||||
* @return \stdClass|null Returns null if the given parameter is not either an array or an object.
|
||||
*/
|
||||
function array_to_object_2025($array = []): ?stdClass
|
||||
{
|
||||
if (is_object($array)) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
if (! is_array($array)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$object = new \stdClass;
|
||||
|
||||
if (! isset($array) || empty($array)) {
|
||||
return $object;
|
||||
}
|
||||
|
||||
foreach ($array as $k => $v) {
|
||||
if (mb_strlen($k)) {
|
||||
if (is_array($v)) {
|
||||
if (array_is_assoc($v)) {
|
||||
// Convert associative arrays to objects
|
||||
$object->{$k} = array_to_object_2025($v);
|
||||
} else {
|
||||
// For indexed arrays, keep them as arrays but process their elements
|
||||
$object->{$k} = [];
|
||||
foreach ($v as $idx => $item) {
|
||||
if (is_array($item)) {
|
||||
$object->{$k}[$idx] = array_to_object_2025($item);
|
||||
} else {
|
||||
$object->{$k}[$idx] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$object->{$k} = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('array_is_assoc')) {
|
||||
/**
|
||||
* Determines whether or not an array is an associative array.
|
||||
*
|
||||
* @param array $array The array to be evaluated.
|
||||
*/
|
||||
function array_is_assoc($array): bool
|
||||
{
|
||||
if (! is_array($array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($array === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_keys($array) !== range(0, count($array) - 1);
|
||||
}
|
||||
}
|
||||
37
app/Helpers/Global/comparision_helpers.php
Normal file
37
app/Helpers/Global/comparision_helpers.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('is_empty')) {
|
||||
/**
|
||||
* A better function to check if a value is empty or null. Strings, arrays, and Objects are supported.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
function is_empty($value): bool
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if ($value === '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
if (count($value) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
|
||||
if (count($value) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
8
app/Helpers/Global/generation_helpers.php
Normal file
8
app/Helpers/Global/generation_helpers.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('epoch_now_timestamp')) {
|
||||
function epoch_now_timestamp($multiplier = 1000)
|
||||
{
|
||||
return (int) round(microtime(true) * $multiplier);
|
||||
}
|
||||
}
|
||||
6
app/Helpers/Global/helpers.php
Normal file
6
app/Helpers/Global/helpers.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
include 'comparision_helpers.php';
|
||||
include 'cast_helpers.php';
|
||||
include 'generation_helpers.php';
|
||||
include 'user_access_helpers.php';
|
||||
58
app/Helpers/Global/user_access_helpers.php
Normal file
58
app/Helpers/Global/user_access_helpers.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
if (! function_exists('user_is_master_admin')) {
|
||||
|
||||
function user_is_master_admin(User $user)
|
||||
{
|
||||
$emails = ['autopilotshorts@gmail.com', 'team@autopilotshorts.com', 'charles@exastellar.com'];
|
||||
$user_id = 1;
|
||||
|
||||
if ($user->id == $user_id) {
|
||||
return true;
|
||||
} elseif (in_array($user->email, $emails)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('user_is_blocked_from_purchase')) {
|
||||
|
||||
function user_is_blocked_from_purchase(User $user)
|
||||
{
|
||||
$emails = ['productionlittlebird@gmail.com'];
|
||||
|
||||
if (in_array($user->email, $emails)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('user_can_generate_demo_videos')) {
|
||||
|
||||
function user_can_generate_demo_videos(User $user)
|
||||
{
|
||||
return user_is_master_admin($user);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('user_is_external_reviewer')) {
|
||||
|
||||
function user_is_external_reviewer(User $user)
|
||||
{
|
||||
$emails = [
|
||||
'shaebaelish.623259@gmail.com',
|
||||
];
|
||||
|
||||
if (in_array($user->email, $emails)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
13
app/Http/Controllers/AdminDashboardController.php
Normal file
13
app/Http/Controllers/AdminDashboardController.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Inertia\Inertia;
|
||||
|
||||
class AdminDashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return Inertia::render('admin/dashboard');
|
||||
}
|
||||
}
|
||||
51
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
51
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the login page.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('auth/login', [
|
||||
'canResetPassword' => Route::has('password.request'),
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
41
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
41
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ConfirmablePasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the confirm password page.
|
||||
*/
|
||||
public function show(): Response
|
||||
{
|
||||
return Inertia::render('auth/confirm-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's password.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => $request->user()->email,
|
||||
'password' => $request->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send a new email verification notification.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with('status', 'verification-link-sent');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class EmailVerificationPromptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the email verification prompt page.
|
||||
*/
|
||||
public function __invoke(Request $request): Response|RedirectResponse
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(route('dashboard', absolute: false))
|
||||
: Inertia::render('auth/verify-email', ['status' => $request->session()->get('status')]);
|
||||
}
|
||||
}
|
||||
69
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
69
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the password reset page.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('auth/reset-password', [
|
||||
'email' => $request->email,
|
||||
'token' => $request->route('token'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => 'required',
|
||||
'email' => 'required|email',
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
if ($status == Password::PasswordReset) {
|
||||
return to_route('login')->with('status', __($status));
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [__($status)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
41
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
41
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the password reset link request page.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('auth/forgot-password', [
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
return back()->with('status', __('A reset link will be sent if the account exists.'));
|
||||
}
|
||||
}
|
||||
51
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
51
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the registration page.
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('auth/register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return to_route('dashboard');
|
||||
}
|
||||
}
|
||||
137
app/Http/Controllers/Auth/SanctumAuthController.php
Normal file
137
app/Http/Controllers/Auth/SanctumAuthController.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class SanctumAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Register a new user and return a token
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'password' => ['required', 'confirmed', Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
$token = $user->createToken('auth_token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'success' => [
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'user' => $user,
|
||||
],
|
||||
'message' => 'Registration completed successfully.',
|
||||
],
|
||||
], 201);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'data' => $e->errors(),
|
||||
'message' => 'Please review your inputs before submitting again.',
|
||||
],
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'data' => [],
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login a user and return a token
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
if (! Auth::attempt($request->only('email', 'password'))) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'data' => [],
|
||||
'message' => 'Invalid credentials provided.',
|
||||
],
|
||||
], 401);
|
||||
}
|
||||
|
||||
$user = User::where('email', $request->email)->firstOrFail();
|
||||
$token = $user->createToken('auth_token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'success' => [
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'user' => $user,
|
||||
],
|
||||
'message' => 'Authentication successful.',
|
||||
],
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'data' => $e->errors(),
|
||||
'message' => 'Please review your inputs before submitting again.',
|
||||
],
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'data' => [],
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the user (revoke the token)
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => [
|
||||
'data' => [],
|
||||
'message' => 'Successfully signed out.',
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'data' => [],
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
30
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
/** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */
|
||||
$user = $request->user();
|
||||
|
||||
event(new Verified($user));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
18
app/Http/Controllers/FrontHomeController.php
Normal file
18
app/Http/Controllers/FrontHomeController.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class FrontHomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
if (App::environment('local')) {
|
||||
return Inertia::render('welcome');
|
||||
}
|
||||
|
||||
return Inertia::render('comingsoon');
|
||||
}
|
||||
}
|
||||
377
app/Http/Controllers/RenderController.php
Normal file
377
app/Http/Controllers/RenderController.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\FirstParty\Render\RenderConstants;
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use App\Models\VideoCaption;
|
||||
use App\Models\VideoElement;
|
||||
use App\Models\VideoRender;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use stdClass;
|
||||
use Str;
|
||||
|
||||
class RenderController extends Controller
|
||||
{
|
||||
public function getVideoElements(Request $request, string $uuid)
|
||||
{
|
||||
if (! Str::isUuid($uuid)) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'message' => 'Invalid UUID format.',
|
||||
],
|
||||
], 400);
|
||||
}
|
||||
|
||||
$video = Video::with('video_elements')->where('uuid', $uuid)->first();
|
||||
|
||||
if (! $video) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'message' => 'Video not found.',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json((object) [
|
||||
'success' => [
|
||||
'data' => [
|
||||
'video_elements' => $video->video_elements,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
if (! $video_render) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'message' => 'Video render not found.',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$video = Video::where('id', $video_render->video_id)->first();
|
||||
|
||||
if (! $video) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'message' => 'Video not found.',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function startRender(Request $request)
|
||||
{
|
||||
|
||||
$video_render_request = array_to_object_2025($request->all());
|
||||
|
||||
$video_render_action = $this->saveUserVideoRenderRequest(Auth::user(), $video_render_request);
|
||||
|
||||
if (! $video_render_action->success) {
|
||||
$error_message = $video_render_action?->message ? $video_render_action->message : 'Unable to render, possibly because the video is already being rendered. Check external ID.';
|
||||
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'message' => $error_message,
|
||||
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// Create a video
|
||||
return response()->json((object) [
|
||||
'success' => [
|
||||
'data' => [
|
||||
'uuid' => $video_render_action->model->uuid,
|
||||
'status' => $video_render_action->model->status,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function renderStatus(Request $request, string $uuid)
|
||||
{
|
||||
if (! Str::isUuid($uuid)) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'message' => 'Invalid UUID.',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$video_render = VideoRender::where('uuid', $uuid)->first();
|
||||
|
||||
if (! $video_render) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'message' => 'Video render not found.',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json((object) [
|
||||
'success' => [
|
||||
'data' => [
|
||||
'uuid' => $video_render->uuid,
|
||||
'status' => $video_render->status,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function allRenders(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$video_renders = VideoRender::where('user_id', $user->id)
|
||||
->orderBy('id', 'desc')->get();
|
||||
|
||||
return response()->json((object) [
|
||||
'success' => [
|
||||
'data' => [
|
||||
'video_renders' => $video_renders,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function saveUserVideoRenderRequest(User $user, stdClass $video_render_request)
|
||||
{
|
||||
// check if there is an existing video render request with the same external id
|
||||
|
||||
$video_render_is_busy = VideoRender::where('user_id', $user->id)
|
||||
->where('external_id', $video_render_request->external_id)
|
||||
->whereIn('status', [
|
||||
RenderConstants::STATUS_PLANNED,
|
||||
RenderConstants::STATUS_WAITING,
|
||||
RenderConstants::STATUS_TRANSCRIBING,
|
||||
RenderConstants::STATUS_RENDERING,
|
||||
])->first();
|
||||
|
||||
if ($video_render_is_busy) {
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'message' => 'Video is already in queue or rendering. Status: '.$video_render_is_busy->status,
|
||||
];
|
||||
}
|
||||
// dd($video_render_request);
|
||||
|
||||
$video = $this->getUserVideoByExternalId($user, $video_render_request->external_id);
|
||||
|
||||
$video = $this->updateVideoWithRenderRequest($video, $video_render_request);
|
||||
|
||||
$video = $this->saveUserVideo($video);
|
||||
|
||||
$this->saveVideoCaptions($video);
|
||||
|
||||
try {
|
||||
$this->saveVideoElements($video);
|
||||
} catch (\Exception $e) {
|
||||
return (object) [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
$new_video_render = VideoRender::create([
|
||||
'user_id' => $user->id,
|
||||
'video_id' => $video->id,
|
||||
'external_id' => $video_render_request->external_id,
|
||||
'payload' => $video_render_request,
|
||||
'status' => RenderConstants::STATUS_PLANNED,
|
||||
]);
|
||||
|
||||
return (object) [
|
||||
'success' => true,
|
||||
'model' => $new_video_render,
|
||||
];
|
||||
}
|
||||
|
||||
private function saveVideoCaptions(Video $video)
|
||||
{
|
||||
VideoCaption::where('video_id', $video->id)->delete();
|
||||
|
||||
if (isset($video->payload->captions)) {
|
||||
foreach ($video->payload->captions as $caption) {
|
||||
$video_caption = new VideoCaption;
|
||||
$video_caption->video_id = $video->id;
|
||||
$video_caption->time = $caption->time;
|
||||
$video_caption->duration = $caption->duration;
|
||||
$video_caption->text = $caption->text;
|
||||
|
||||
if (isset($caption->parameters)) {
|
||||
$video_caption->parameters = $caption->parameters;
|
||||
}
|
||||
|
||||
$video_caption->words = $caption->words;
|
||||
$video_caption->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function saveVideoElements(Video $video)
|
||||
{
|
||||
if (isset($video->payload->elements)) {
|
||||
$existing_video_elements = VideoElement::where('video_id', $video->id)->get();
|
||||
|
||||
// Create a lookup array of existing elements by asset hash, but keep ALL matching elements
|
||||
$existing_elements_by_hash = [];
|
||||
foreach ($existing_video_elements as $existing_element) {
|
||||
if (! isset($existing_elements_by_hash[$existing_element->asset_hash])) {
|
||||
$existing_elements_by_hash[$existing_element->asset_hash] = [];
|
||||
}
|
||||
$existing_elements_by_hash[$existing_element->asset_hash][] = $existing_element;
|
||||
}
|
||||
|
||||
// Track which elements we're keeping
|
||||
$kept_element_ids = [];
|
||||
// Track which hashes we've already processed to handle duplicates
|
||||
$processed_hashes = [];
|
||||
|
||||
// Validate element URL if exist
|
||||
foreach ($video->payload->elements as $element) {
|
||||
if (isset($element->url)) {
|
||||
if (! $this->validateElementUrl($element->url)) {
|
||||
throw new \Exception('Invalid URL: '.$element->url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
foreach ($video->payload->elements as $element) {
|
||||
$asset_hash = $this->getAssetHash($video, $element->url);
|
||||
|
||||
if (isset($existing_elements_by_hash[$asset_hash]) && count($existing_elements_by_hash[$asset_hash]) > 0) {
|
||||
// Get the next unused element with this hash
|
||||
$unused_elements = array_filter($existing_elements_by_hash[$asset_hash], function ($elem) use ($kept_element_ids) {
|
||||
return ! in_array($elem->id, $kept_element_ids);
|
||||
});
|
||||
|
||||
if (count($unused_elements) > 0) {
|
||||
// Use the first unused element
|
||||
$video_element = reset($unused_elements);
|
||||
$kept_element_ids[] = $video_element->id;
|
||||
} else {
|
||||
// All elements with this hash are already used, create a new one
|
||||
$video_element = new VideoElement;
|
||||
$video_element->video_id = $video->id;
|
||||
$video_element->asset_hash = $asset_hash;
|
||||
$video_element->original_asset_url = $element->url;
|
||||
}
|
||||
} else {
|
||||
// No elements with this hash, create a new one
|
||||
$video_element = new VideoElement;
|
||||
$video_element->video_id = $video->id;
|
||||
$video_element->asset_hash = $asset_hash;
|
||||
$video_element->original_asset_url = $element->url;
|
||||
}
|
||||
|
||||
if (isset($element->external_reference) && ! is_empty($element->external_reference)) {
|
||||
$video_element->external_reference = $element->external_reference;
|
||||
}
|
||||
$video_element->type = $element->type;
|
||||
$video_element->time = $element->time;
|
||||
$video_element->track = $element->track;
|
||||
$video_element->duration = $element->duration;
|
||||
|
||||
if (isset($element->parameters)) {
|
||||
$video_element->parameters = $element->parameters;
|
||||
}
|
||||
|
||||
$video_element->save();
|
||||
|
||||
// Add newly created ID to kept list if needed
|
||||
if (! in_array($video_element->id, $kept_element_ids)) {
|
||||
$kept_element_ids[] = $video_element->id;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete elements that weren't in the payload
|
||||
if (count($existing_video_elements) > 0) {
|
||||
VideoElement::where('video_id', $video->id)
|
||||
->whereNotIn('id', $kept_element_ids)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getAssetHash(Video $video, $url)
|
||||
{
|
||||
return hash('sha256', $video->id.'-'.$url);
|
||||
}
|
||||
|
||||
private function validateElementUrl(string $url)
|
||||
{
|
||||
// First check if it's a valid URL format
|
||||
$validator = Validator::make(['url' => $url], [
|
||||
'url' => 'required|url',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// validate url by making a http head request, return true | false boolean
|
||||
try {
|
||||
// Using Laravel's HTTP client to make a HEAD request
|
||||
$response = Http::withOptions([
|
||||
'timeout' => 10,
|
||||
'connect_timeout' => 5,
|
||||
'verify' => true,
|
||||
'http_errors' => false, // Don't throw exceptions for 4xx/5xx responses
|
||||
])->head($url);
|
||||
|
||||
// Check if the response is successful (2xx status code) or a redirect (3xx)
|
||||
return $response->successful();
|
||||
} catch (\Exception $e) {
|
||||
// Catch any exceptions (connection issues, invalid URLs, etc.)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function saveUserVideo(Video $video)
|
||||
{
|
||||
if ($video->isDirty()) {
|
||||
$video->save();
|
||||
}
|
||||
|
||||
return $video;
|
||||
}
|
||||
|
||||
private function updateVideoWithRenderRequest(Video $video, stdClass $video_render_request)
|
||||
{
|
||||
// dd($video_render_request);
|
||||
|
||||
$video->content_type = $video_render_request->content_type;
|
||||
$video->width = $video_render_request->width;
|
||||
$video->height = $video_render_request->height;
|
||||
$video->aspect_ratio = $video_render_request->aspect_ratio;
|
||||
$video->payload = $video_render_request;
|
||||
$video->render_settings = (object) [
|
||||
'video_bitrate' => $video_render_request->video_bitrate,
|
||||
'audio_bitrate' => $video_render_request->audio_bitrate,
|
||||
'fps' => $video_render_request->fps,
|
||||
];
|
||||
|
||||
return $video;
|
||||
}
|
||||
|
||||
private function getUserVideoByExternalId(User $user, string $external_id)
|
||||
{
|
||||
$video = Video::where('user_id', $user->id)
|
||||
->where('external_id', $external_id)
|
||||
->first();
|
||||
|
||||
if (! $video) {
|
||||
|
||||
$video = new Video;
|
||||
$video->user_id = $user->id;
|
||||
$video->external_id = $external_id;
|
||||
}
|
||||
|
||||
return $video;
|
||||
}
|
||||
}
|
||||
39
app/Http/Controllers/Settings/PasswordController.php
Normal file
39
app/Http/Controllers/Settings/PasswordController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user's password settings page.
|
||||
*/
|
||||
public function edit(): Response
|
||||
{
|
||||
return Inertia::render('settings/password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
63
app/Http/Controllers/Settings/ProfileController.php
Normal file
63
app/Http/Controllers/Settings/ProfileController.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Settings\ProfileUpdateRequest;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user's profile settings page.
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
return Inertia::render('settings/profile', [
|
||||
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile settings.
|
||||
*/
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return to_route('profile.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's account.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/TestController.php
Normal file
27
app/Http/Controllers/TestController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\RunVideoRenderPipelineJob;
|
||||
use App\Models\Video;
|
||||
|
||||
class TestController extends Controller
|
||||
{
|
||||
public function RunVideoRenderPipelineJob()
|
||||
{
|
||||
$dispatch_job = true;
|
||||
$video = Video::latest()->first();
|
||||
|
||||
if ($video) {
|
||||
|
||||
if ($dispatch_job) {
|
||||
RunVideoRenderPipelineJob::dispatch($video->id)->onQueue('render');
|
||||
} else {
|
||||
$job = new RunVideoRenderPipelineJob($video->id);
|
||||
$job->handle();
|
||||
}
|
||||
} else {
|
||||
echo 'NO VIDEO';
|
||||
}
|
||||
}
|
||||
}
|
||||
13
app/Http/Controllers/UserDashboardController.php
Normal file
13
app/Http/Controllers/UserDashboardController.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Inertia\Inertia;
|
||||
|
||||
class UserDashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return Inertia::render('dashboard');
|
||||
}
|
||||
}
|
||||
23
app/Http/Middleware/AdminMiddleware.php
Normal file
23
app/Http/Middleware/AdminMiddleware.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AdminMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (user_is_master_admin($request->user())) {
|
||||
return $next($request);
|
||||
}
|
||||
abort(403, 'You are not authorized to perform this action.');
|
||||
}
|
||||
}
|
||||
23
app/Http/Middleware/HandleAppearance.php
Normal file
23
app/Http/Middleware/HandleAppearance.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HandleAppearance
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
57
app/Http/Middleware/HandleInertiaRequests.php
Normal file
57
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
use Tighten\Ziggy\Ziggy;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that's loaded on the first page visit.
|
||||
*
|
||||
* @see https://inertiajs.com/server-side-setup#root-template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determines the current asset version.
|
||||
*
|
||||
* @see https://inertiajs.com/asset-versioning
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
|
||||
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'quote' => ['message' => trim($message), 'author' => trim($author)],
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
'user_is_admin' => user_is_master_admin($request->user()),
|
||||
],
|
||||
'ziggy' => fn (): array => [
|
||||
...(new Ziggy)->toArray(),
|
||||
'location' => $request->url(),
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
}
|
||||
}
|
||||
85
app/Http/Requests/Auth/LoginRequest.php
Normal file
85
app/Http/Requests/Auth/LoginRequest.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
32
app/Http/Requests/Settings/ProfileUpdateRequest.php
Normal file
32
app/Http/Requests/Settings/ProfileUpdateRequest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
79
app/Jobs/RunVideoRenderPipelineJob.php
Normal file
79
app/Jobs/RunVideoRenderPipelineJob.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Helpers\FirstParty\MediaEngine\MediaEngine;
|
||||
use App\Helpers\FirstParty\Render\FfmpegVideoRenderer;
|
||||
use App\Helpers\FirstParty\Render\RenderConstants;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RunVideoRenderPipelineJob implements ShouldQueue
|
||||
{
|
||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $video_id;
|
||||
|
||||
public $timeout = 300;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(int $video_id)
|
||||
{
|
||||
$this->onQueue('general_video');
|
||||
$this->video_id = $video_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->batch()?->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$video = Video::with('video_elements', 'latest_render')->find($this->video_id);
|
||||
|
||||
if (! $video) {
|
||||
return;
|
||||
}
|
||||
|
||||
$video_render = $video->latest_render;
|
||||
$video_render->status = RenderConstants::STATUS_RENDERING;
|
||||
$video_render->processing_started_at = now();
|
||||
$video_render->save();
|
||||
|
||||
$output = FfmpegVideoRenderer::render($video);
|
||||
|
||||
if ($output->success) {
|
||||
$video_render->status = RenderConstants::STATUS_SUCCEEDED;
|
||||
|
||||
$saved_media = MediaEngine::addMedia(
|
||||
MediaEngine::getCollectionKeyByOwnerMediaType('user', 'video'),
|
||||
'video',
|
||||
MediaEngine::USER_RENDERED,
|
||||
MediaEngine::USER,
|
||||
$output->name,
|
||||
file_get_contents($output->path),
|
||||
);
|
||||
|
||||
$video_render->completed_video_uuid = $saved_media->uuid;
|
||||
$video_render->completed_video_full_url = MediaEngine::getMediaCloudUrl($saved_media);
|
||||
$video_render->processing_finished_at = now();
|
||||
$video_render->save();
|
||||
} else {
|
||||
$video_render->processing_finished_at = now();
|
||||
$video_render->status = RenderConstants::STATUS_FAILED;
|
||||
$video_render->save();
|
||||
|
||||
throw $output->exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
app/Jobs/SaveVideoElementsBatchJob.php
Normal file
78
app/Jobs/SaveVideoElementsBatchJob.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Helpers\FirstParty\MediaEngine\MediaEngine;
|
||||
use App\Models\Media;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class SaveVideoElementsBatchJob implements ShouldQueue
|
||||
{
|
||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $video_id;
|
||||
|
||||
public $timeout = 300;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(int $video_id)
|
||||
{
|
||||
$this->onQueue('general_video');
|
||||
$this->video_id = $video_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->batch()?->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$video = Video::with('video_elements')->find($this->video_id);
|
||||
|
||||
if (! $video) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($video->video_elements as $video_element) {
|
||||
|
||||
if (! is_empty($video_element->asset_uuid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// dump($video_element);
|
||||
|
||||
// Media Details: Filename, extension and mimetype
|
||||
$media_details = MediaEngine::getFileDetailsbyUrl($video_element->original_asset_url);
|
||||
|
||||
// Media Content: Blob
|
||||
$media_content = file_get_contents($video_element->original_asset_url);
|
||||
|
||||
// Media Filename: generate a new filename
|
||||
$media_filename = $video_element->type.'_'.epoch_now_timestamp().'.'.$media_details->extension;
|
||||
|
||||
$saved_media = MediaEngine::addMedia(
|
||||
MediaEngine::getCollectionKeyByOwnerMediaType('user', $video_element->type),
|
||||
$video_element->type,
|
||||
MediaEngine::USER_UPLOADED,
|
||||
MediaEngine::USER,
|
||||
$media_filename,
|
||||
$media_content,
|
||||
);
|
||||
|
||||
$video_element->asset_uuid = $saved_media->uuid;
|
||||
$video_element->asset_url = MediaEngine::getMediaCloudUrl($saved_media);
|
||||
$video_element->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
75
app/Models/Media.php
Normal file
75
app/Models/Media.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Class Media
|
||||
*
|
||||
* @property int $id
|
||||
* @property uuid|null $uuid
|
||||
* @property int $media_collection_id
|
||||
* @property int|null $user_id
|
||||
* @property string $media_type
|
||||
* @property string $media_source
|
||||
* @property string $media_provider
|
||||
* @property string $name
|
||||
* @property string $mime_type
|
||||
* @property string $file_name
|
||||
* @property string $file_path
|
||||
* @property string $disk
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property MediaCollection $media_collection
|
||||
*/
|
||||
class Media extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'medias';
|
||||
|
||||
protected $casts = [
|
||||
'media_collection_id' => 'int',
|
||||
'user_id' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'media_collection_id',
|
||||
'user_id',
|
||||
'media_type',
|
||||
'media_source',
|
||||
'media_provider',
|
||||
'mime_type',
|
||||
'file_name',
|
||||
'file_path',
|
||||
'disk',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'media_url',
|
||||
];
|
||||
|
||||
public function media_collection()
|
||||
{
|
||||
return $this->belongsTo(MediaCollection::class);
|
||||
}
|
||||
|
||||
protected function mediaUrl(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function ($value, $attributes) {
|
||||
return Storage::disk($attributes['disk'])->url($attributes['file_path'].$attributes['file_name']);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
44
app/Models/MediaCollection.php
Normal file
44
app/Models/MediaCollection.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class MediaCollection
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $key
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
* @property bool $is_system
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Collection|Media[] $media
|
||||
*/
|
||||
class MediaCollection extends Model
|
||||
{
|
||||
protected $table = 'media_collections';
|
||||
|
||||
protected $casts = [
|
||||
'is_system' => 'bool',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'name',
|
||||
'description',
|
||||
'is_system',
|
||||
];
|
||||
|
||||
public function media()
|
||||
{
|
||||
return $this->hasMany(Media::class);
|
||||
}
|
||||
}
|
||||
62
app/Models/User.php
Normal file
62
app/Models/User.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Str;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'email',
|
||||
'password',
|
||||
'uuid',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The "booted" method of the model.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
$model->uuid = $model->uuid ?? (string) Str::uuid();
|
||||
});
|
||||
}
|
||||
}
|
||||
80
app/Models/Video.php
Normal file
80
app/Models/Video.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Str;
|
||||
|
||||
/**
|
||||
* Class Video
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $external_id
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Collection|VideoRender[] $video_renders
|
||||
*/
|
||||
class Video extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'videos';
|
||||
|
||||
protected $casts = [
|
||||
'width' => 'int',
|
||||
'height' => 'int',
|
||||
'payload' => 'object',
|
||||
'render_settings' => 'object',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'external_id',
|
||||
'content_type',
|
||||
'width',
|
||||
'height',
|
||||
'aspect_ratio',
|
||||
'payload',
|
||||
'render_settings',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'id',
|
||||
];
|
||||
|
||||
public function video_renders()
|
||||
{
|
||||
return $this->hasMany(VideoRender::class)->orderBy('id', 'DESC');
|
||||
}
|
||||
|
||||
public function video_captions()
|
||||
{
|
||||
return $this->hasMany(VideoCaption::class)->orderBy('time', 'ASC');
|
||||
}
|
||||
|
||||
public function video_elements()
|
||||
{
|
||||
return $this->hasMany(VideoElement::class)->orderBy('time', 'ASC');
|
||||
}
|
||||
|
||||
public function latest_render()
|
||||
{
|
||||
return $this->hasOne(VideoRender::class)->latest();
|
||||
}
|
||||
|
||||
/**
|
||||
* The "booted" method of the model.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
$model->uuid = $model->uuid ?? (string) Str::uuid();
|
||||
});
|
||||
}
|
||||
}
|
||||
56
app/Models/VideoCaption.php
Normal file
56
app/Models/VideoCaption.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* Class VideoCaption
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $video_id
|
||||
* @property float $time
|
||||
* @property float $duration
|
||||
* @property string $text
|
||||
* @property string $parameters
|
||||
* @property string $payload
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property string|null $deleted_at
|
||||
* @property Video $video
|
||||
*/
|
||||
class VideoCaption extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'video_captions';
|
||||
|
||||
protected $casts = [
|
||||
'video_id' => 'int',
|
||||
'time' => 'float',
|
||||
'duration' => 'float',
|
||||
'parameters' => 'object',
|
||||
'words' => 'object',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'video_id',
|
||||
'time',
|
||||
'duration',
|
||||
|
||||
'text',
|
||||
'parameters',
|
||||
'words',
|
||||
];
|
||||
|
||||
public function video()
|
||||
{
|
||||
return $this->belongsTo(Video::class);
|
||||
}
|
||||
}
|
||||
54
app/Models/VideoElement.php
Normal file
54
app/Models/VideoElement.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* Class VideoElement
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $original_asset_url
|
||||
* @property uuid|null $asset_uuid
|
||||
* @property string|null $asset_url
|
||||
* @property string $type
|
||||
* @property float $time
|
||||
* @property int $track
|
||||
* @property float $duration
|
||||
* @property string $parameters
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
*/
|
||||
class VideoElement extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'video_elements';
|
||||
|
||||
protected $casts = [
|
||||
'time' => 'float',
|
||||
'track' => 'int',
|
||||
'duration' => 'float',
|
||||
'parameters' => 'object',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'parent_element_id',
|
||||
'external_reference',
|
||||
'asset_hash',
|
||||
'original_asset_url',
|
||||
'asset_uuid',
|
||||
'asset_url',
|
||||
'type',
|
||||
'time',
|
||||
'track',
|
||||
'duration',
|
||||
'parameters',
|
||||
];
|
||||
}
|
||||
83
app/Models/VideoRender.php
Normal file
83
app/Models/VideoRender.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Str;
|
||||
|
||||
/**
|
||||
* Class VideoRender
|
||||
*
|
||||
* @property int $id
|
||||
* @property uuid $uuid
|
||||
* @property string|null $external_id
|
||||
* @property int|null $video_id
|
||||
* @property int|null $user_id
|
||||
* @property string $payload
|
||||
* @property string $status
|
||||
* @property Carbon|null $processing_started_at
|
||||
* @property Carbon|null $processing_finished_at
|
||||
* @property uuid|null $completed_video_uuid
|
||||
* @property string|null $completed_video_full_url
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Video|null $video
|
||||
* @property User|null $user
|
||||
*/
|
||||
class VideoRender extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'video_renders';
|
||||
|
||||
protected $casts = [
|
||||
'video_id' => 'int',
|
||||
'user_id' => 'int',
|
||||
'processing_started_at' => 'datetime',
|
||||
'processing_finished_at' => 'datetime',
|
||||
'payload' => 'object',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'external_id',
|
||||
'video_id',
|
||||
'user_id',
|
||||
'payload',
|
||||
'status',
|
||||
'processing_started_at',
|
||||
'processing_finished_at',
|
||||
'completed_video_uuid',
|
||||
'completed_video_full_url',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'id',
|
||||
];
|
||||
|
||||
public function video()
|
||||
{
|
||||
return $this->belongsTo(Video::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The "booted" method of the model.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
$model->uuid = $model->uuid ?? (string) Str::uuid();
|
||||
});
|
||||
}
|
||||
}
|
||||
24
app/Providers/AppServiceProvider.php
Normal file
24
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
36
app/Providers/HorizonServiceProvider.php
Normal file
36
app/Providers/HorizonServiceProvider.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Horizon\Horizon;
|
||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||
|
||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Horizon::routeSmsNotificationsTo('15556667777');
|
||||
// Horizon::routeMailNotificationsTo('example@example.com');
|
||||
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Horizon gate.
|
||||
*
|
||||
* This gate determines who can access Horizon in non-local environments.
|
||||
*/
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewHorizon', function ($user) {
|
||||
return in_array($user->email, [
|
||||
//
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user