--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace yii_app\commands;
+
+use Yii;
+use yii\console\Controller;
+use yii\console\ExitCode;
+use yii_app\records\Files;
+
+/**
+ * Массовая конвертация MOV → MP4 для существующих файлов.
+ *
+ * Запуск:
+ * php yii convert-video/run — конвертация всех MOV
+ * php yii convert-video/run --limit=50 — конвертация первых 50
+ * php yii convert-video/run --dry-run — только показать что будет конвертировано
+ * php yii convert-video/status — статистика MOV/MP4
+ *
+ * Рекомендуется запускать ночью через cron с nice:
+ * 0 2 * * * nice -n 19 php /var/www/erp24/yii convert-video/run --limit=200 >> /var/log/convert-video.log 2>&1
+ */
+class ConvertVideoController extends Controller
+{
+ /** @var int Максимум файлов за один запуск (0 = без лимита) */
+ public int $limit = 0;
+
+ /** @var bool Только показать, не конвертировать */
+ public bool $dryRun = false;
+
+ /** @var int Пауза между файлами в секундах (снижает нагрузку) */
+ public int $sleep = 2;
+
+ public function options($actionID): array
+ {
+ return array_merge(parent::options($actionID), [
+ 'limit',
+ 'dryRun',
+ 'sleep',
+ ]);
+ }
+
+ public function optionAliases(): array
+ {
+ return [
+ 'l' => 'limit',
+ 'n' => 'dryRun',
+ 's' => 'sleep',
+ ];
+ }
+
+ /**
+ * Показать статистику MOV/MP4 файлов в БД.
+ */
+ public function actionStatus(): int
+ {
+ $movCount = Files::find()->where(['like', 'url', '.mov'])->count();
+ $mp4Count = Files::find()->where(['like', 'url', '.mp4'])->count();
+ $aviCount = Files::find()->where(['like', 'url', '.avi'])->count();
+
+ $this->stdout("=== Статистика видеофайлов ===\n");
+ $this->stdout("MOV: {$movCount}\n");
+ $this->stdout("MP4: {$mp4Count}\n");
+ $this->stdout("AVI: {$aviCount}\n");
+ $this->stdout("Всего к конвертации: " . ($movCount + $aviCount) . "\n");
+
+ return ExitCode::OK;
+ }
+
+ /**
+ * Конвертация MOV/AVI → MP4 с throttling.
+ */
+ public function actionRun(): int
+ {
+ // Проверка FFmpeg
+ exec('which ffmpeg 2>/dev/null', $output, $code);
+ if ($code !== 0) {
+ $this->stderr("FFmpeg не установлен!\n");
+ return ExitCode::UNSPECIFIED_ERROR;
+ }
+
+ $query = Files::find()
+ ->where(['like', 'url', '.mov'])
+ ->orWhere(['like', 'url', '.avi'])
+ ->orderBy(['id' => SORT_ASC]);
+
+ if ($this->limit > 0) {
+ $query->limit($this->limit);
+ }
+
+ /** @var Files[] $files */
+ $files = $query->all();
+ $total = count($files);
+
+ if ($total === 0) {
+ $this->stdout("Нет файлов для конвертации.\n");
+ return ExitCode::OK;
+ }
+
+ $this->stdout("Найдено файлов: {$total}\n");
+ if ($this->dryRun) {
+ $this->stdout("Режим dry-run: конвертация не будет выполнена\n\n");
+ }
+
+ $converted = 0;
+ $skipped = 0;
+ $errors = 0;
+ $basePath = Yii::getAlias('@app') . '/web';
+
+ foreach ($files as $i => $file) {
+ $num = $i + 1;
+ $sourcePath = $basePath . $file->url;
+ $mp4Url = preg_replace('/\.(mov|avi)$/i', '.mp4', $file->url);
+ $mp4Path = $basePath . $mp4Url;
+
+ $this->stdout("[{$num}/{$total}] #{$file->id} {$file->url}");
+
+ // Файл уже не существует
+ if (!file_exists($sourcePath)) {
+ $this->stdout(" — ПРОПУЩЕН (файл не найден)\n");
+ $skipped++;
+ continue;
+ }
+
+ // MP4 уже есть
+ if (file_exists($mp4Path)) {
+ $this->stdout(" — ПРОПУЩЕН (MP4 уже существует)\n");
+ // Обновить URL в БД если ещё не обновлён
+ if ($file->url !== $mp4Url) {
+ if (!$this->dryRun) {
+ $file->url = $mp4Url;
+ $file->save(false);
+ }
+ $this->stdout(" → URL обновлён в БД\n");
+ }
+ $skipped++;
+ continue;
+ }
+
+ $size = filesize($sourcePath);
+ $sizeMb = round($size / 1024 / 1024, 1);
+ $this->stdout(" ({$sizeMb} MB)");
+
+ if ($this->dryRun) {
+ $this->stdout(" — будет конвертирован\n");
+ $converted++;
+ continue;
+ }
+
+ // Конвертация
+ $cmd = sprintf(
+ 'nice -n 19 timeout 600 ffmpeg -y -i %s -c:v libx264 -preset fast -crf 23 -c:a aac -movflags +faststart %s 2>&1',
+ escapeshellarg($sourcePath),
+ escapeshellarg($mp4Path)
+ );
+
+ $startTime = microtime(true);
+ exec($cmd, $cmdOutput, $returnCode);
+ $elapsed = round(microtime(true) - $startTime, 1);
+
+ if ($returnCode !== 0 || !file_exists($mp4Path)) {
+ $error = implode("\n", array_slice($cmdOutput, -5));
+ $this->stderr(" — ОШИБКА ({$elapsed}s, code={$returnCode})\n");
+ Yii::error("ConvertVideo: ошибка #{$file->id}: {$error}", 'video');
+ if (file_exists($mp4Path)) {
+ @unlink($mp4Path);
+ }
+ $errors++;
+ } else {
+ // Обновить URL в БД
+ $file->url = $mp4Url;
+ if ($file->save(false)) {
+ // Удалить оригинал
+ @unlink($sourcePath);
+ $mp4Size = round(filesize($mp4Path) / 1024 / 1024, 1);
+ $this->stdout(" — OK ({$elapsed}s, {$sizeMb}→{$mp4Size} MB)\n");
+ $converted++;
+ } else {
+ $this->stderr(" — ОШИБКА сохранения БД\n");
+ $errors++;
+ }
+ }
+
+ $cmdOutput = [];
+
+ // Пауза между файлами
+ if ($this->sleep > 0 && $i < $total - 1) {
+ sleep($this->sleep);
+ }
+ }
+
+ $this->stdout("\n=== Итоги ===\n");
+ $this->stdout("Конвертировано: {$converted}\n");
+ $this->stdout("Пропущено: {$skipped}\n");
+ $this->stdout("Ошибок: {$errors}\n");
+
+ return $errors > 0 ? ExitCode::UNSPECIFIED_ERROR : ExitCode::OK;
+ }
+}