]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
feat(ERP-236): add console command for batch MOV→MP4 conversion
authorAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Tue, 24 Feb 2026 14:09:59 +0000 (17:09 +0300)
committerAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Tue, 24 Feb 2026 14:09:59 +0000 (17:09 +0300)
php yii convert-video/status   — show stats
php yii convert-video/run      — convert all
php yii convert-video/run --limit=50 --dry-run — preview

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
erp24/commands/ConvertVideoController.php [new file with mode: 0644]

diff --git a/erp24/commands/ConvertVideoController.php b/erp24/commands/ConvertVideoController.php
new file mode 100644 (file)
index 0000000..2724ce0
--- /dev/null
@@ -0,0 +1,200 @@
+<?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;
+    }
+}