feat: auto-retry SVG covers every 30 min

coverRetry.js: сканирует articles с cover < 30KB (SVG-заглушки),
перегенерирует через covers.generateCover(). При недоступности
провайдера (timeout/502) прерывает цикл до следующего запуска.
Первый запуск через 5 мин после старта engine, далее каждые 30 мин.
This commit is contained in:
Ник (Claude)
2026-06-10 08:53:39 +03:00
parent ad133027d0
commit d8a901131c
3 changed files with 89 additions and 1 deletions
+4
View File
@@ -91,6 +91,10 @@ const start = async () => {
await migrate();
await config.reloadAi();
console.log('[Engine] AI config loaded from app_settings: text=' + config.ai.baseUrl + ', images=' + config.ai.imageBaseUrl);
// Автоматический ретрай SVG-заглушек
require('./src/services/coverRetry').start();
app.listen(config.port, () => {
console.log(`[Engine] Running on port ${config.port}`);
});
+1 -1
View File
@@ -5,7 +5,7 @@ const covers = require('./src/services/covers');
const config = require('./src/config');
const { query } = require('./src/config/db');
const DELAY_MS = 8000;
const DELAY_MS = 20000;
const sleep = ms => new Promise(r => setTimeout(r, ms));
(async () => {
+84
View File
@@ -0,0 +1,84 @@
// Фоновый ретрай SVG-обложек.
// Запускается каждые 30 минут, ищет статьи с маленькими обложками (< 30KB),
// перегенерирует их. Если провайдер недоступен — пропускает до следующего запуска.
const { query } = require('../config/db');
const covers = require('../services/covers');
const config = require('../config');
const path = require('path');
const fs = require('fs');
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
const SVG_MAX_BYTES = 30 * 1024; // < 30KB = скорее всего SVG-заглушка
const DELAY_MS = 25000; // пауза между генерациями
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function findSvgCovers() {
const { rows } = await query(
`SELECT id, title, tags, cover_url FROM articles
WHERE cover_url IS NOT NULL AND cover_url != ''
ORDER BY id DESC`
);
const candidates = [];
for (const a of rows) {
const file = path.join(UPLOADS_DIR, a.cover_url.replace('/uploads/', ''));
try {
const stat = fs.statSync(file);
if (stat.size < SVG_MAX_BYTES) candidates.push(a);
} catch (_) { /* файл не найден — пропускаем */ }
}
return candidates;
}
async function retryCovers() {
let candidates;
try { candidates = await findSvgCovers(); }
catch (err) { console.warn('[CoverRetry] DB error:', err.message); return; }
if (candidates.length === 0) {
console.log('[CoverRetry] no SVG covers found');
return;
}
console.log(`[CoverRetry] found ${candidates.length} SVG cover(s), retrying...`);
let ok = 0, fail = 0;
for (const a of candidates) {
try {
await covers.generateCover({ articleId: a.id, title: a.title, tags: a.tags || [], channelId: 1 });
// Проверяем что новый файл реально больше
const { rows } = await query('SELECT cover_url FROM articles WHERE id=$1', [a.id]);
const newFile = path.join(UPLOADS_DIR, (rows[0]?.cover_url || '').replace('/uploads/', ''));
const newSize = fs.existsSync(newFile) ? fs.statSync(newFile).size : 0;
if (newSize > SVG_MAX_BYTES) {
console.log(`[CoverRetry] ✓ article=${a.id} new size=${Math.round(newSize/1024)}KB`);
ok++;
} else {
console.log(`[CoverRetry] ✗ article=${a.id} still small (${Math.round(newSize/1024)}KB)`);
fail++;
}
} catch (err) {
console.warn(`[CoverRetry] ✗ article=${a.id} error:`, err.message.slice(0, 80));
fail++;
// Если провайдер не отвечает — прерываем цикл до следующего запуска
if (err.message.includes('timeout') || err.message.includes('502')) {
console.warn('[CoverRetry] provider unavailable, stopping until next run');
break;
}
}
if (ok + fail < candidates.length) await sleep(DELAY_MS);
}
console.log(`[CoverRetry] done: ${ok} fixed, ${fail} failed`);
}
// Запуск каждые 30 минут
function start() {
// Первый запуск через 5 минут после старта engine (чтобы не грузить при рестарте)
setTimeout(async () => {
await retryCovers();
setInterval(retryCovers, 30 * 60 * 1000);
}, 5 * 60 * 1000);
console.log('[CoverRetry] Auto-retry started (every 30 min, first run in 5 min)');
}
module.exports = { start, retryCovers };