From e4381bacf920a4df34576b1980a133a582374f95 Mon Sep 17 00:00:00 2001 From: Aleksey Filippov Date: Wed, 25 Feb 2026 14:23:38 +0300 Subject: [PATCH] =?utf8?q?feat(ERP-236):=20refine=20ConvertVideoController?= =?utf8?q?=20=E2=80=94=20entity=20filter,=20min-age=20window,=20proc=5Fope?= =?utf8?q?n=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - Filter by entity=write_offs_products_erp_video (only write-off videos, not all files) - Add --min-age=2h param: skip files younger than 2h (queue still processing) - Reduce default --days from 10 to 2 (cron safety net for queue failures) - Fix PHP hanging: switch from exec/system to proc_open with /dev/null pipes - Fix timeout: raise from 600s to 1800s for large files - Update actionStatus: show write-off video stats only (MOV+AVI / MP4) - Update WriteOffsErp: change border date from -2 month to -1 month - Add WriteOffsAttachmentsController: sync border_date comment - Add erp24/docs/diagrams/convert-video-controller.html Co-Authored-By: Claude Sonnet 4.6 --- erp24/commands/ConvertVideoController.php | 78 ++- .../diagrams/convert-video-controller.html | 576 ++++++++++++++++++ 2 files changed, 625 insertions(+), 29 deletions(-) create mode 100644 erp24/docs/diagrams/convert-video-controller.html diff --git a/erp24/commands/ConvertVideoController.php b/erp24/commands/ConvertVideoController.php index 7c7ce17d..f377435e 100644 --- a/erp24/commands/ConvertVideoController.php +++ b/erp24/commands/ConvertVideoController.php @@ -10,20 +10,24 @@ use yii\console\ExitCode; use yii_app\records\Files; /** - * Массовая конвертация MOV → MP4 для существующих файлов. + * Конвертация MOV → MP4 для файлов, пропущенных очередью. * - * По умолчанию конвертирует только файлы младше 1 месяца (старые удаляются через write-offs-attachments). + * Новые файлы конвертируются автоматически через ConvertVideoToMp4Job при загрузке. + * Эта команда — страховка для файлов, которые очередь не обработала. + * + * По умолчанию берёт файлы за последние 2 дня (--days=2), но не новее 2 часов (--min-age=2), + * чтобы не мешать очереди, которая ещё не успела обработать только что загруженные файлы. * * Запуск: - * php yii convert-video/run — конвертация MOV младше 1 месяца - * php yii convert-video/run --days=7 — только за последние 7 дней - * php yii convert-video/run --days=0 — все файлы без фильтра по дате - * php yii convert-video/run --limit=50 — конвертация первых 50 + * php yii convert-video/run — файлы от 2 ч до 2 дней назад (для cron) + * php yii convert-video/run --days=0 — все файлы (для разовой массовой конвертации) + * php yii convert-video/run --min-age=0 — включая только что загруженные + * 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 + * Cron (каждый час — страховка для очереди): + * 0 * * * * sudo -u www-data php /var/www/erp24/yii convert-video/run --limit=50 >> /var/log/convert-video.log 2>&1 */ class ConvertVideoController extends Controller { @@ -36,8 +40,11 @@ class ConvertVideoController extends Controller /** @var int Пауза между файлами в секундах (снижает нагрузку) */ public int $sleep = 2; - /** @var int Конвертировать файлы за последние N дней (0 = все, по умолчанию 10) */ - public int $days = 10; + /** @var int Конвертировать файлы за последние N дней (0 = все, по умолчанию 2) */ + public int $days = 2; + + /** @var int Пропускать файлы моложе N часов (очередь ещё не успела, по умолчанию 2) */ + public int $minAge = 2; public function options($actionID): array { @@ -46,6 +53,7 @@ class ConvertVideoController extends Controller 'dryRun', 'sleep', 'days', + 'minAge', ]); } @@ -66,30 +74,28 @@ class ConvertVideoController extends Controller { $borderDate = $this->days > 0 ? date('Y-m-d H:i:s', strtotime("-{$this->days} days")) : null; - $movQuery = Files::find()->where(['like', 'url', '.mov']); - $mp4Query = Files::find()->where(['like', 'url', '.mp4']); - $aviQuery = Files::find()->where(['like', 'url', '.avi']); + $entity = \yii_app\records\WriteOffsProductsErp::WRITE_OFFS_VIDEO; + + $movQuery = Files::find()->where(['entity' => $entity])->andWhere(['or', ['like', 'url', '.mov'], ['like', 'url', '.avi']]); + $mp4Query = Files::find()->where(['entity' => $entity])->andWhere(['like', 'url', '.mp4']); if ($borderDate) { $movQuery->andWhere(['>=', 'created_at', $borderDate]); $mp4Query->andWhere(['>=', 'created_at', $borderDate]); - $aviQuery->andWhere(['>=', 'created_at', $borderDate]); } $movCount = $movQuery->count(); $mp4Count = $mp4Query->count(); - $aviCount = $aviQuery->count(); - $this->stdout("=== Статистика видеофайлов ===\n"); + $this->stdout("=== Статистика видео списаний ===\n"); + $this->stdout("entity: {$entity}\n"); if ($borderDate) { $this->stdout("Фильтр: за последние {$this->days} дней (с {$borderDate})\n"); } else { $this->stdout("Фильтр: все файлы\n"); } - $this->stdout("MOV: {$movCount}\n"); - $this->stdout("MP4: {$mp4Count}\n"); - $this->stdout("AVI: {$aviCount}\n"); - $this->stdout("Всего к конвертации: " . ($movCount + $aviCount) . "\n"); + $this->stdout("MOV (к конвертации): {$movCount}\n"); + $this->stdout("MP4 (уже готово): {$mp4Count}\n"); return ExitCode::OK; } @@ -107,14 +113,27 @@ class ConvertVideoController extends Controller } $borderDate = $this->days > 0 ? date('Y-m-d H:i:s', strtotime("-{$this->days} days")) : null; + // Пропускаем файлы моложе minAge часов — очередь ещё может их обрабатывать + $maxDate = $this->minAge > 0 ? date('Y-m-d H:i:s', strtotime("-{$this->minAge} hours")) : null; $query = Files::find() + ->andWhere(['entity' => \yii_app\records\WriteOffsProductsErp::WRITE_OFFS_VIDEO]) ->andWhere(['or', ['like', 'url', '.mov'], ['like', 'url', '.avi']]) ->orderBy(['id' => SORT_DESC]); if ($borderDate) { $query->andWhere(['>=', 'created_at', $borderDate]); + } + if ($maxDate) { + $query->andWhere(['<=', 'created_at', $maxDate]); + } + + if ($borderDate && $maxDate) { + $this->stdout("Фильтр: от {$this->minAge}ч до {$this->days}д назад ({$maxDate} — {$borderDate})\n"); + } elseif ($borderDate) { $this->stdout("Фильтр: за последние {$this->days} дней (с {$borderDate})\n"); + } elseif ($maxDate) { + $this->stdout("Фильтр: все файлы старше {$this->minAge}ч (до {$maxDate})\n"); } else { $this->stdout("Фильтр: все файлы\n"); } @@ -182,24 +201,25 @@ class ConvertVideoController extends Controller continue; } - // Конвертация (вывод в лог-файл, чтобы не переполнять буфер exec) - $logFile = Yii::getAlias('@runtime') . '/ffmpeg_last.log'; + // Конвертация через proc_open (избегаем зависание PHP на буфере) $cmd = sprintf( - 'nice -n 19 timeout 1800 ffmpeg -y -i %s -c:v libx264 -preset fast -crf 23 -c:a aac -movflags +faststart %s >%s 2>&1', + 'nice -n 19 timeout 1800 ffmpeg -y -i %s -c:v libx264 -preset fast -crf 23 -c:a aac -movflags +faststart %s', escapeshellarg($sourcePath), - escapeshellarg($mp4Path), - escapeshellarg($logFile) + escapeshellarg($mp4Path) ); $startTime = microtime(true); - $returnCode = 0; - system($cmd, $returnCode); + $proc = proc_open($cmd, [ + 0 => ['file', '/dev/null', 'r'], + 1 => ['file', '/dev/null', 'w'], + 2 => ['file', '/dev/null', 'w'], + ], $pipes); + $returnCode = proc_close($proc); $elapsed = round(microtime(true) - $startTime, 1); if ($returnCode !== 0 || !file_exists($mp4Path)) { - $error = is_file($logFile) ? implode("\n", array_slice(file($logFile, FILE_IGNORE_NEW_LINES), -5)) : ''; $this->stderr(" — ОШИБКА ({$elapsed}s, code={$returnCode})\n"); - Yii::error("ConvertVideo: ошибка #{$file->id}: {$error}", 'video'); + Yii::error("ConvertVideo: ошибка #{$file->id}, code={$returnCode}", 'video'); if (file_exists($mp4Path)) { @unlink($mp4Path); } diff --git a/erp24/docs/diagrams/convert-video-controller.html b/erp24/docs/diagrams/convert-video-controller.html new file mode 100644 index 00000000..c9400bfa --- /dev/null +++ b/erp24/docs/diagrams/convert-video-controller.html @@ -0,0 +1,576 @@ + + + + + +ConvertVideoController — логика конвертации MOV → MP4 + + + + + + + +
+ + +
+

ConvertVideoControllerYii2 Console

+

erp24/commands/ConvertVideoController.php — конвертация MOV/AVI → MP4

+
+ + + +
+ +
+
upload
+
📤
+
FileService
+
saveFile()
MOV/AVI → push job
+
+ +
→
+ +
+
queue
+
🐇
+
RabbitMQ
+
ConvertVideoToMp4Job
telegram-queue
+
+ +
→
+ +
+
worker
+
⚙️
+
supervisord
+
3 процесса
TTR=600, attempts=3
+
+ +
→
+ +
+
success
+
✅
+
files.url → .mp4
+
MOV удалён
сжатие ~3-4×
+
+ +
↘
+ +
+
fallback
+
⏰
+
ConvertVideoController
+
cron каждый час
страховка очереди
+
+ +
+ + + +
+
+
+
слишком старые
+
✓ берём
+
очередь
+
now
+
borderDate
2 дня назад
+
maxDate
2 часа назад
+
+
+
> 2 дней — не трогаем (export-old удалит)
+
2ч — 2дн назад — пропущены очередью, конвертируем
+
< 2 часов — очередь ещё работает, не мешаем
+
+
+ + + +
+
+
--days
+
по умолчанию: 2
+
Не брать файлы старше N дней. 0 = все файлы (для разовой массовой конвертации)
+
+
+
--min-age
+
по умолчанию: 2
+
Пропускать файлы моложе N часов. Очередь ещё успеет их обработать
+
+
+
--limit
+
по умолчанию: 0 (без лимита)
+
Максимум файлов за один запуск
+
+
+
--sleep
+
по умолчанию: 2
+
Пауза в секундах между файлами. Снижает нагрузку на диск и CPU
+
+
+
--dry-run
+
по умолчанию: false
+
Только показать что будет конвертировано. Ничего не меняет
+
+
+ + + +
+
+ + + +
+
+flowchart TD
+    A([▶ actionRun]) --> B["exec('which ffmpeg')"]
+    B --> C{FFmpeg\nустановлен?}
+    C -->|нет| D([🛑 exit ERROR])
+    C -->|да| E["Вычислить временно́е окно\nborderDate = NOW - days\nmaxDate = NOW - minAge hours"]
+    E --> F["SQL: SELECT files\nWHERE url LIKE '%.mov'\n  OR url LIKE '%.avi'\nAND created_at BETWEEN\n  borderDate AND maxDate\nORDER BY id DESC LIMIT N"]
+    F --> G{Найдено\nфайлов?}
+    G -->|0| H([✅ exit OK\nНет файлов])
+    G -->|"> 0"| I["foreach files"]
+    I --> J{"file_exists\nsourcePath?"}
+    J -->|нет| K["⏭ ПРОПУЩЕН\nфайл не найден"]
+    J -->|да| L{"file_exists\nmp4Path?"}
+    L -->|да| M["⏭ ПРОПУЩЕН\nMP4 уже есть"]
+    M --> N{"files.url\n!= mp4Url?"}
+    N -->|да| O["✏️ files.url = mp4Url\nsave() — синхронизация БД"]
+    N -->|нет| P
+    O --> P
+    L -->|нет| Q{"--dry-run?"}
+    Q -->|да| R["📋 будет конвертирован\nbez изменений"]
+    Q -->|нет| S["proc_open:\nnice -n 19 timeout 1800\nffmpeg -y -i source.mov\n-c:v libx264 -preset fast\n-crf 23 -c:a aac\n-movflags +faststart\noutput.mp4\n\nstdin/stdout/stderr → /dev/null"]
+    S --> T{returnCode == 0\nAND mp4 exists?}
+    T -->|нет| U["❌ ОШИБКА\nunlink битого mp4\nYii::error(code=X)"]
+    T -->|да| V["✅ files.url = .mp4\nfile.save()"]
+    V --> W["unlink source.mov\nOK (Xs, A→B MB)"]
+    W --> P
+    U --> P
+    K --> P
+    R --> P
+    P["sleep(2s)\n⏸ пауза"] --> X{Ещё\nфайлы?}
+    X -->|да| I
+    X -->|нет| Y["=== Итоги ===\nКонвертировано: N\nПропущено: N\nОшибок: N"]
+    Y --> Z([✅ exit OK])
+
+    classDef process  fill:#2a3045,stroke:#88c0d0,color:#d8def0
+    classDef decision fill:#2e2a1f,stroke:#ebcb8b,color:#ebcb8b
+    classDef success  fill:#1f2e20,stroke:#a3be8c,color:#a3be8c
+    classDef error    fill:#2e1f20,stroke:#bf616a,color:#bf616a
+    classDef skip     fill:#252535,stroke:#b48ead,color:#b48ead
+    classDef terminal fill:#1a1f2e,stroke:#7a86a8,color:#d8def0
+
+    class A,Z,H,D terminal
+    class B,E,F,I,P,S,V process
+    class C,G,J,L,N,Q,T,X decision
+    class W,O success
+    class U error
+    class K,M,R skip
+    
+
+ +
+
Процесс / действие
+
Решение / ветвление
+
Успех
+
Ошибка
+
Пропуск
+
+ + +
+ FFmpeg команда: + nice -n 19 timeout 1800 ffmpeg -y -i source.mov -c:v libx264 -preset fast -crf 23 -c:a aac -movflags +faststart output.mp4

+ nice -n 19 — низкий приоритет CPU, не мешает пользователям  |  + timeout 1800 — принудительный kill через 30 мин  |  + crf 23 — баланс качество/размер  |  + +faststart — MP4 начинает играть до полной загрузки  |  + proc_open + /dev/null — PHP не зависает на буфере вывода FFmpeg +
+ + +
+ Cron (каждый час, страховка очереди):
+ 0 * * * * sudo -u www-data php /var/www/erp24/yii convert-video/run --limit=50 >> /var/log/convert-video.log 2>&1 +
+ +
+ + + + + + -- 2.39.5