diff --git a/.env.example b/.env.example index 47d5672..438e487 100644 --- a/.env.example +++ b/.env.example @@ -71,3 +71,5 @@ FFPROBE_BINARIES=/usr/bin/ffprobe REVERB_APP_ID=my-app-id REVERB_APP_KEY=my-app-key REVERB_APP_SECRET=my-app-secret + +YT_DLP_PATH= diff --git a/app/Conversion/MediaOperations/AudioQualityFilterOperation.php b/app/Conversion/MediaOperations/AudioQualityFilterOperation.php index 2035a5e..8051546 100644 --- a/app/Conversion/MediaOperations/AudioQualityFilterOperation.php +++ b/app/Conversion/MediaOperations/AudioQualityFilterOperation.php @@ -13,7 +13,7 @@ class AudioQualityFilterOperation implements MediaFormatOperation { public Conversion $conversion; - private int $currentBitrate; + private ?int $currentBitrate; public function __construct(Conversion $conversion) { @@ -23,6 +23,10 @@ public function __construct(Conversion $conversion) public function applyToFormat(DefaultVideo $format): DefaultVideo { + if ($this->conversion->audio === false || $this->currentBitrate === null) { + return $format; + } + $maxQuality = $this->conversion->audio_quality; if ($maxQuality < 0 || $maxQuality > 1) { @@ -44,7 +48,7 @@ public function applyToFormat(DefaultVideo $format): DefaultVideo private function prepareData(): void { $probe = app(FFProbe::class); - + $this->currentBitrate = null; $streams = $probe->streams(Storage::disk($this->conversion->file->disk)->path($this->conversion->file->filename)); foreach ($streams as $stream) { if ($stream->get('codec_type') === 'audio') { diff --git a/app/Conversion/MediaOperations/AutoCropFilterOperation.php b/app/Conversion/MediaOperations/AutoCropFilterOperation.php index 2f19365..6dc1240 100644 --- a/app/Conversion/MediaOperations/AutoCropFilterOperation.php +++ b/app/Conversion/MediaOperations/AutoCropFilterOperation.php @@ -32,7 +32,7 @@ private function prepareData(): void $path = Storage::disk($this->conversion->file->disk)->path($this->conversion->file->filename); $ffmpeg = config('laravel-ffmpeg.ffmpeg.binaries'); - $process = Process::run("{$ffmpeg} -flags2 +export_mvs -i {$path} -vf cropdetect=mode=mvedges -f null - 2>&1 | awk '/crop/ { print \$NF }' | tail -1"); + $process = Process::run("{$ffmpeg} -flags2 +export_mvs -i {$path} -vf cropdetect -f null - 2>&1 | awk '/crop/ { print \$NF }' | tail -1"); $this->crop = trim($process->output()); } diff --git a/app/Conversion/MediaOperations/MaxSizeOperation.php b/app/Conversion/MediaOperations/MaxSizeOperation.php index 8bffe66..f62c67d 100644 --- a/app/Conversion/MediaOperations/MaxSizeOperation.php +++ b/app/Conversion/MediaOperations/MaxSizeOperation.php @@ -36,7 +36,7 @@ public function applyToFormat(DefaultVideo $format): DefaultVideo $maxSizeInBits = $maxSizeInMB * 1024 * 1024 * 8; $videoBitrate = floor(($maxSizeInBits / $duration) - floor($audioBitrateInKiloBits)); - if ($videoBitrate > $this->format->get('bit_rate')) { + if (($videoBitrate * $duration) >= $this->format->get('bit_rate')) { return $format; } diff --git a/app/Enums/ConversionStatus.php b/app/Enums/ConversionStatus.php index 8438873..accdd49 100644 --- a/app/Enums/ConversionStatus.php +++ b/app/Enums/ConversionStatus.php @@ -4,6 +4,8 @@ enum ConversionStatus { + public const DOWNLOADING = 'downloading'; + public const PREPARING = 'preparing'; public const PENDING = 'pending'; diff --git a/app/Events/ConversionFinished.php b/app/Events/ConversionFinished.php index 440fefd..0d5c455 100644 --- a/app/Events/ConversionFinished.php +++ b/app/Events/ConversionFinished.php @@ -5,10 +5,11 @@ use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class ConversionFinished implements ShouldBroadcast +class ConversionFinished implements ShouldBroadcast, ShouldQueue { use Dispatchable; use InteractsWithSockets; diff --git a/app/Events/ConversionProgressEvent.php b/app/Events/ConversionProgressEvent.php index 07a67d9..a684db2 100644 --- a/app/Events/ConversionProgressEvent.php +++ b/app/Events/ConversionProgressEvent.php @@ -6,10 +6,11 @@ use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class ConversionProgressEvent implements ShouldBroadcast +class ConversionProgressEvent implements ShouldBroadcast, ShouldQueue { use Dispatchable; use InteractsWithSockets; @@ -35,7 +36,7 @@ public function __construct( $this->percentage = $percentage; $this->remaining = $remaining; $this->rate = $rate; - $this->sessionId = Conversion::find($conversionId)->file->session_id; + $this->sessionId = Conversion::find($conversionId)->session_id; } public function broadcastOn(): array diff --git a/app/Events/ConversionUpdated.php b/app/Events/ConversionUpdated.php index 5b4fcd8..3c43730 100644 --- a/app/Events/ConversionUpdated.php +++ b/app/Events/ConversionUpdated.php @@ -6,10 +6,11 @@ use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class ConversionUpdated implements ShouldBroadcast +class ConversionUpdated implements ShouldBroadcast, ShouldQueue { use Dispatchable; use InteractsWithSockets; @@ -26,7 +27,7 @@ public function __construct(string $conversionId) $this->conversionId = $conversionId; $conversion = Conversion::with('file')->where('id', $conversionId)->first(); $this->conversion = $conversion->toArray(); - $this->sessionId = Conversion::find($conversionId)->file->session_id; + $this->sessionId = Conversion::find($conversionId)->session_id; } public function broadcastOn(): array diff --git a/app/Events/DownloadProgress.php b/app/Events/DownloadProgress.php new file mode 100644 index 0000000..edda9c4 --- /dev/null +++ b/app/Events/DownloadProgress.php @@ -0,0 +1,54 @@ +conversionId = $conversionId; + $conversion = Conversion::find($this->conversionId); + $this->sessionId = $conversion->session_id; + $this->progressTarget = $progressTarget; + $this->percentage = $percentage; + $this->size = $size; + $this->speed = $speed; + $this->eta = $eta; + $this->totalTime = $totalTime; + } + + public function broadcastOn(): array + { + return [ + new Channel('session.' . $this->sessionId), + ]; + } +} diff --git a/app/Events/FileUploadFailed.php b/app/Events/FileUploadFailed.php index b5abeee..52b4c64 100644 --- a/app/Events/FileUploadFailed.php +++ b/app/Events/FileUploadFailed.php @@ -5,10 +5,11 @@ use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class FileUploadFailed implements ShouldBroadcast +class FileUploadFailed implements ShouldBroadcast, ShouldQueue { use Dispatchable; use InteractsWithSockets; @@ -16,11 +17,6 @@ class FileUploadFailed implements ShouldBroadcast public function __construct(public string $sessionId) {} - /** - * Get the channels the event should broadcast on. - * - * @return array - */ public function broadcastOn(): array { return [ diff --git a/app/Events/FileUploadSuccessful.php b/app/Events/FileUploadSuccessful.php index eb7cd52..df15ed2 100644 --- a/app/Events/FileUploadSuccessful.php +++ b/app/Events/FileUploadSuccessful.php @@ -5,10 +5,11 @@ use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class FileUploadSuccessful implements ShouldBroadcast +class FileUploadSuccessful implements ShouldBroadcast, ShouldQueue { use Dispatchable; use InteractsWithSockets; diff --git a/app/Events/PreviousFilesDeleted.php b/app/Events/PreviousFilesDeleted.php index dee1109..c71a4e6 100644 --- a/app/Events/PreviousFilesDeleted.php +++ b/app/Events/PreviousFilesDeleted.php @@ -5,10 +5,11 @@ use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class PreviousFilesDeleted implements ShouldBroadcast +class PreviousFilesDeleted implements ShouldBroadcast, ShouldQueue { use Dispatchable; use InteractsWithSockets; diff --git a/app/Http/Controllers/ListConverterController.php b/app/Http/Controllers/ListConverterController.php index f58ba01..241ca4e 100644 --- a/app/Http/Controllers/ListConverterController.php +++ b/app/Http/Controllers/ListConverterController.php @@ -18,20 +18,16 @@ public function __construct(Request $request) public function index(Request $request): Response { - $conversions = Conversion::with('file')->whereHas('file', function ($query) { - $query->where('session_id', $this->sessionId); - })->get(); + $conversions = Conversion::with('file')->where('session_id', $this->sessionId)->get(); return Inertia::render('Converter/List', [ - 'conversions' => $conversions, + 'conversions' => $conversions->toArray(), ]); } public function myConversions(Request $request): array { - $conversions = Conversion::with('file')->whereHas('file', function ($query) { - $query->where('session_id', $this->sessionId); - })->get(); + $conversions = Conversion::with('file')->where('session_id', $this->sessionId)->get(); return $conversions->toArray(); } diff --git a/app/Http/Controllers/StartConverterController.php b/app/Http/Controllers/StartConverterController.php index e1c1915..54b75be 100644 --- a/app/Http/Controllers/StartConverterController.php +++ b/app/Http/Controllers/StartConverterController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Conversion\ConversionSettings; +use App\Enums\ConversionStatus; use App\Events\FileUploadFailed; use App\Events\FileUploadSuccessful; use App\Events\PreviousFilesDeleted; @@ -28,29 +29,32 @@ public function __invoke(StartConverterRequest $request) $this->deleteOldFiles(); - $file = $this->handleUploadedFile($request); + if ($request->hasFile('file')) { + $file = $this->handleUploadedFile($request); + } $conversionSettings = ConversionSettings::fromRequest($validated); $conversion = Conversion::create([ ...$conversionSettings->toArray(), - 'file_id' => $file->id, + 'status' => ConversionStatus::PENDING, + 'session_id' => $this->sessionId, + 'file_id' => $file->id ?? null, + 'url' => $validated['url'] ?? null, ]); //ConversionJob::dispatchSync($conversion->id); - ConversionJob::dispatch($conversion->id)->onQueue('converter'); + if ($request->hasFile('file')) { + ConversionJob::dispatch($conversion->id)->onQueue('converter'); + } return redirect()->route('conversions.list'); } private function handleUploadedFile(StartConverterRequest $request): File { - $file = null; - - if ($request->hasFile('file')) { - $file = $this->storeUploadedFile($request->file('file')); - } + $file = $this->storeUploadedFile($request->file('file')); if ($file === null) { FileUploadFailed::dispatch($this->sessionId); @@ -87,6 +91,7 @@ private function deleteOldFiles(): void { $countOldFiles = File::where('session_id', $this->sessionId)->count(); + Conversion::where('session_id', $this->sessionId)->delete(); File::where('session_id', $this->sessionId)->delete(); if ($countOldFiles > 0) { diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 974e4df..efc6185 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -3,7 +3,6 @@ namespace App\Http\Middleware; use App\Models\Conversion; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; use Inertia\Middleware; @@ -42,9 +41,7 @@ public function share(Request $request): array 'session' => [ 'id' => $request->session()->getId(), ], - 'conversions' => fn () => Conversion::whereHas('file', static function (Builder $query) use ($request) { - $query->where('session_id', $request->session()->getId()); - })->select('id')->get(), + 'conversions' => fn () => Conversion::where('session_id', $request->session()->getId())->select('id')->get(), ]); } } diff --git a/app/Jobs/ConversionJob.php b/app/Jobs/ConversionJob.php index a4d5fec..d7436f8 100644 --- a/app/Jobs/ConversionJob.php +++ b/app/Jobs/ConversionJob.php @@ -7,6 +7,7 @@ use FFMpeg\Format\Video\X264; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; @@ -15,6 +16,7 @@ class ConversionJob implements ShouldBeUnique, ShouldQueue { + use Dispatchable; use Queueable; private string $conversionId; diff --git a/app/Jobs/DownloadVideoJob.php b/app/Jobs/DownloadVideoJob.php new file mode 100644 index 0000000..5a02978 --- /dev/null +++ b/app/Jobs/DownloadVideoJob.php @@ -0,0 +1,120 @@ +conversionId); + + if ($conversion === null) { + return; + } + try { + $conversion->update([ + 'status' => ConversionStatus::DOWNLOADING, + ]); + + $youtubeDl = app(YoutubeDl::class); + + $youtubeDl->onProgress(function (?string $progressTarget, ?string $percentage = null, ?string $size = null, ?string $speed = null, ?string $eta = null, ?string $totalTime = null) use ($conversion): void { + $iPercentage = $percentage !== null ? (int) str_replace('%', '', $percentage) : null; + + if ($iPercentage % 5 !== 0) { + return; + } + + DownloadProgress::dispatch($conversion->id, $progressTarget, $percentage, $size, $speed, $eta, $totalTime); + }); + + // only supporting one video for now + $video = $youtubeDl->download( + Options::create() + ->downloadPath(Storage::disk('conversions')->path('/')) + ->restrictFileNames(true) + ->continue(true) + ->format('best') + ->noPlaylist() + ->cleanupMetadata(true) + ->maxDownloads(1) + ->url($conversion->url) + )->getVideos()[0] ?? null; + + if ($video === null || $video->getError() !== null) { + $conversion->update([ + 'status' => ConversionStatus::FAILED, + ]); + + Log::error('Failed to download video', [ + 'conversion_id' => $conversion->id, + 'error' => $video->getError(), + ]); + + return; + } + + $fileName = Str::uuid()->toString() . '.' . $video->getFile()->getExtension(); + $exists = Storage::disk('conversions')->exists($video->getFile()->getFilename()); + + $moved = \Illuminate\Support\Facades\File::move($video->getFile()->getPathname(), Storage::disk('conversions')->path($fileName)); + + if (! $exists || ! $moved) { + $conversion->update([ + 'status' => ConversionStatus::FAILED, + ]); + + Log::error('Failed to download video', [ + 'conversion_id' => $conversion->id, + ]); + + return; + } + + $file = File::create([ + 'filename' => $fileName, + 'disk' => 'conversions', + 'mime_type' => Storage::disk('conversions')->mimeType($fileName), + 'size' => Storage::disk('conversions')->size($fileName), + 'extension' => pathinfo($fileName, PATHINFO_EXTENSION), + 'session_id' => $conversion->session_id, + ]); + + $conversion->update([ + 'file_id' => $file->id, + 'status' => ConversionStatus::PREPARING, + ]); + + ConversionJob::dispatch($conversion->id)->onQueue('converter'); + } catch (Throwable $th) { + $conversion->update([ + 'status' => ConversionStatus::FAILED, + ]); + + Log::error('Failed to download video', [ + 'conversion_id' => $conversion->id, + 'error' => $th->getMessage(), + ]); + } + } +} diff --git a/app/Models/Conversion.php b/app/Models/Conversion.php index b6c001b..1aed3bd 100644 --- a/app/Models/Conversion.php +++ b/app/Models/Conversion.php @@ -32,6 +32,11 @@ class Conversion extends Model protected $casts = [ 'audio' => 'boolean', + 'interpolation' => 'boolean', + 'auto_crop' => 'boolean', + 'max_size' => 'integer', + 'audio_quality' => 'float', + 'watermark' => 'boolean', 'downloadable' => 'boolean', ]; @@ -62,6 +67,15 @@ public function getProgress(): array ], [ 'order' => 2, + 'completed' => $this->status === ConversionStatus::PROCESSING || $this->status === ConversionStatus::PREPARING || $this->status === ConversionStatus::FINISHED || $this->status === ConversionStatus::FAILED || $this->status === ConversionStatus::CANCELED, + 'current_step' => $this->status === ConversionStatus::DOWNLOADING, + 'status' => ConversionStatus::DOWNLOADING, + 'title' => 'Download läuft', + 'description' => 'Es wird versucht das Video herunterzuladen.', + 'visible' => $this->url !== null, + ], + [ + 'order' => 3, 'completed' => $this->status === ConversionStatus::PROCESSING || $this->status === ConversionStatus::FINISHED || $this->status === ConversionStatus::FAILED || $this->status === ConversionStatus::CANCELED, 'current_step' => $this->status === ConversionStatus::PREPARING, 'status' => ConversionStatus::PREPARING, @@ -70,7 +84,7 @@ public function getProgress(): array 'visible' => true, ], [ - 'order' => 3, + 'order' => 4, 'completed' => $this->status === ConversionStatus::FINISHED || $this->status === ConversionStatus::FAILED || $this->status === ConversionStatus::CANCELED, 'current_step' => $this->status === ConversionStatus::PROCESSING, 'status' => ConversionStatus::PROCESSING, @@ -79,7 +93,7 @@ public function getProgress(): array 'visible' => true, ], [ - 'order' => 4, + 'order' => 5, 'completed' => $this->status === ConversionStatus::FINISHED, 'current_step' => $this->status === ConversionStatus::FINISHED, 'status' => ConversionStatus::FINISHED, @@ -88,7 +102,7 @@ public function getProgress(): array 'visible' => $this->status !== 'cancelled' && $this->status !== ConversionStatus::FAILED, ], [ - 'order' => 5, + 'order' => 6, 'completed' => $this->status === ConversionStatus::FAILED, 'current_step' => $this->status === ConversionStatus::FAILED, 'status' => ConversionStatus::FAILED, @@ -97,7 +111,7 @@ public function getProgress(): array 'visible' => $this->status === ConversionStatus::FAILED, ], [ - 'order' => 6, + 'order' => 7, 'completed' => $this->status === ConversionStatus::CANCELED, 'current_step' => $this->status === ConversionStatus::CANCELED, 'status' => ConversionStatus::CANCELED, diff --git a/app/Observers/ConversionObserver.php b/app/Observers/ConversionObserver.php index ebfc5bd..3862740 100644 --- a/app/Observers/ConversionObserver.php +++ b/app/Observers/ConversionObserver.php @@ -5,6 +5,7 @@ use App\Enums\ConversionStatus; use App\Events\ConversionFinished; use App\Events\ConversionUpdated; +use App\Jobs\DownloadVideoJob; use App\Models\Conversion; class ConversionObserver @@ -17,4 +18,12 @@ public function updated(Conversion $conversion): void ConversionFinished::dispatch($conversion->file->session_id); } } + + public function created(Conversion $conversion): void + { + if ($conversion->status === ConversionStatus::PENDING && $conversion->url !== null && $conversion->file_id === null) { + //DownloadVideoJob::dispatchSync($conversion->id); + DownloadVideoJob::dispatch($conversion->id)->onQueue('downloader'); + } + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7a662f9..f66b6b4 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,12 +3,15 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use YoutubeDl\YoutubeDl; class AppServiceProvider extends ServiceProvider { public function register(): void { - // + $this->app->singleton(YoutubeDl::class, function () { + return (new YoutubeDl)->setBinPath(config('converter.binaries.yt-dlp')); + }); } /** diff --git a/composer.json b/composer.json index b809433..171e3ba 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "laravel/horizon": "^5.30", "laravel/reverb": "^1.0", "laravel/tinker": "^2.9", + "norkunas/youtube-dl-php": "^2.9", "pbmedia/laravel-ffmpeg": "^8.6", "tightenco/ziggy": "^2.4" }, diff --git a/composer.lock b/composer.lock index f15101f..dcbe224 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2836c840f91fefada9ac5f90126fd94d", + "content-hash": "478af065107f30d56cf40dce8dcba79c", "packages": [ { "name": "brick/math", @@ -2834,6 +2834,68 @@ }, "time": "2024-10-08T18:51:32+00:00" }, + { + "name": "norkunas/youtube-dl-php", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/norkunas/youtube-dl-php.git", + "reference": "3484eb38c4ef9c97b3b1ea00f6952179702ed2d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/norkunas/youtube-dl-php/zipball/3484eb38c4ef9c97b3b1ea00f6952179702ed2d1", + "reference": "3484eb38c4ef9c97b3b1ea00f6952179702ed2d1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.4.0", + "symfony/filesystem": "^5.1|^6.0|^7.0", + "symfony/polyfill-php80": "^1.28", + "symfony/process": "^5.1|^6.0|^7.0" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6.11", + "php-cs-fixer/shim": "^3.60", + "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "symfony/phpunit-bridge": "^6.4.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "YoutubeDl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tomas Norkūnas", + "email": "norkunas.tom@gmail.com" + } + ], + "description": "youtube-dl / yt-dlp wrapper for php", + "keywords": [ + "youtube", + "youtube-dl", + "yt-dlp" + ], + "support": { + "issues": "https://github.com/norkunas/youtube-dl-php/issues", + "source": "https://github.com/norkunas/youtube-dl-php/tree/v2.9.0" + }, + "time": "2024-12-09T04:50:15+00:00" + }, { "name": "nunomaduro/termwind", "version": "v2.3.0", @@ -5432,6 +5494,72 @@ ], "time": "2024-09-25T14:20:29+00:00" }, + { + "name": "symfony/filesystem", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, { "name": "symfony/finder", "version": "v7.2.0", diff --git a/config/converter.php b/config/converter.php index b73b4b0..51338f9 100644 --- a/config/converter.php +++ b/config/converter.php @@ -10,4 +10,9 @@ 'default_format_operations' => [ ], + 'binaries' => [ + 'ffmpeg' => config('laravel-ffmpeg.ffmpeg.binaries'), + 'ffprobe' => config('laravel-ffmpeg.ffprobe.binaries'), + 'yt-dlp' => env('YT_DLP_PATH', 'yt-dlp'), + ], ]; diff --git a/config/horizon.php b/config/horizon.php index 945bdd3..e2d5096 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -208,6 +208,19 @@ 'timeout' => 1200, 'nice' => 1, ], + 'downloader' => [ + 'connection' => 'redis', + 'queue' => ['downloader'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 1, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 300, + 'tries' => 1, + 'timeout' => 120, + 'nice' => 1, + ], ], 'environments' => [ @@ -222,6 +235,11 @@ 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], + 'downloader' => [ + 'maxProcesses' => 2, + 'balanceMaxShift' => 1, + 'balanceCooldown' => 3, + ], ], 'local' => [ @@ -231,6 +249,9 @@ 'converter' => [ 'maxProcesses' => 1, ], + 'downloader' => [ + 'maxProcesses' => 1, + ], ], ], ]; diff --git a/database/migrations/2024_12_15_143417_create_conversions_table.php b/database/migrations/2024_12_15_143417_create_conversions_table.php index 8f2c859..f8c362a 100644 --- a/database/migrations/2024_12_15_143417_create_conversions_table.php +++ b/database/migrations/2024_12_15_143417_create_conversions_table.php @@ -12,8 +12,8 @@ public function up(): void { Schema::create('conversions', static function (Blueprint $table) { $table->uuid('id')->primary(); - $table->foreignUuid('conversion_presets')->nullable()->constrained()->cascadeOnDelete()->cascadeOnUpdate(); - $table->foreignUuid('file_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + $table->string('session_id')->unique(); + $table->foreignUuid('file_id')->nullable()->constrained()->cascadeOnDelete()->cascadeOnUpdate(); $table->string('status')->default('pending'); $table->boolean('downloadable')->default(false); $table->boolean('keep_resolution')->default(false); @@ -25,7 +25,14 @@ public function up(): void $table->unsignedInteger('trim_start')->nullable(); $table->unsignedInteger('trim_end')->nullable(); $table->unsignedInteger('max_size')->nullable(); + $table->text('url')->nullable(); $table->timestamps(); + + $table->foreign('session_id') + ->references('id') + ->on('sessions') + ->cascadeOnDelete() + ->cascadeOnDelete(); }); } diff --git a/resources/js/Layouts/Layout.vue b/resources/js/Layouts/Layout.vue index 719dfe6..14e3a90 100644 --- a/resources/js/Layouts/Layout.vue +++ b/resources/js/Layouts/Layout.vue @@ -33,7 +33,7 @@ onMounted(() => { toast.error('Konvertierung fehlgeschlagen'); }) .listen('ConversionProgressEvent', (event) => { - toast.loading('Konvertierung Fortschritt: ' + event.percentage + '%'); + toast.info('Konvertierung Fortschritt: ' + event.percentage + '%'); }); }); diff --git a/resources/js/Pages/Converter/List.vue b/resources/js/Pages/Converter/List.vue index a7167ad..1b5f2d3 100644 --- a/resources/js/Pages/Converter/List.vue +++ b/resources/js/Pages/Converter/List.vue @@ -42,8 +42,23 @@ const updateConversionWithProgress = (progressEvent) => { }); }; +const updateConversionWithDownloadProgress = (downloadProgressEvent) => { + allConversions.value = allConversions.value.map((conversion) => { + if (conversion.id === downloadProgressEvent.conversionId) { + if ( + downloadProgressEvent.speed !== null && + downloadProgressEvent.speed !== '' + ) { + conversion.downloadProgressEvent = downloadProgressEvent; + } + } + return conversion; + }); +}; + const updateConversion = (conversion) => { allConversions.value = allConversions.value.map((c) => { + console.log(c); if (c.id === conversion.id) { c = { ...c, ...conversion }; } @@ -57,6 +72,10 @@ onMounted(() => { .listen('ConversionProgressEvent', (event) => { updateConversionWithProgress(event); }) + .listen('DownloadProgress', (event) => { + console.log(event); + updateConversionWithDownloadProgress(event); + }) .listen('ConversionUpdated', (event) => { console.log(event); updateConversion(event.conversion); @@ -76,9 +95,13 @@ onMounted(() => { :key="idx" :class="cn($attrs.class ?? '')"> - {{ conversion.file.filename }} - Hochgeladen {{ conversion.file.created_at_diff }} + {{ conversion.file?.filename ?? 'Noch nicht vorhanden' }} + + + Hochgeladen {{ conversion.file.created_at_diff }} @@ -142,6 +165,18 @@ onMounted(() => {
Fortschritt: {{ conversion.progressEvent.percentage }}% + +
+ Fortschritt: + {{ conversion.downloadProgressEvent.percentage }}
+ Geschwindigkeit: + {{ conversion.downloadProgressEvent.speed }}
+ Verbleibend: {{ conversion.downloadProgressEvent.eta }} +
diff --git a/resources/js/Pages/Converter/Show.vue b/resources/js/Pages/Converter/Show.vue index 93b4b39..c185cce 100644 --- a/resources/js/Pages/Converter/Show.vue +++ b/resources/js/Pages/Converter/Show.vue @@ -54,7 +54,7 @@ const { isOverDropZone } = useDropZone(dropZoneRef, { const formSchema = toTypedSchema( z.object({ file: z.any().optional(), - url: z.string().url('Keine valide URL').optional(), + url: z.string().url('Keine valide URL').optional().default(''), // keepResolution: z.boolean().default(false), audio: z.boolean().default(true), audioQuality: z