Files
f0ckm/scripts/reconvert_emojis.mjs

120 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
/**
* Reconvert all existing emoji images to WebP.
* - Converts non-WebP files (PNG, GIF, JPG, etc.) to lossless WebP via ImageMagick.
* - Keeps transparency. Handles animated GIFs via -coalesce.
* - Updates the `url` column in `custom_emojis` to the new .webp path.
* - Deletes the original file after successful conversion.
*
* Usage:
* STORAGE_DIR=f0ckm-data DB_HOST=localhost DB_PORT=5454 node scripts/reconvert_emojis.mjs
*/
import db from "../src/inc/sql.mjs";
import cfg from "../src/inc/config.mjs";
import { promises as fs } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { execFile as _execFile } from "child_process";
import { promisify } from "util";
const execFile = promisify(_execFile);
// Custom IM policy that raises list-length from 128 → 65536 to handle large animated GIFs.
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MAGICK_POLICY_PATH = path.resolve(__dirname, '../config/magick-policy');
const magickEnv = { ...process.env, MAGICK_CONFIGURE_PATH: MAGICK_POLICY_PATH };
const emojisDir = cfg.paths.emojis;
console.log(`[reconvert_emojis] Emoji directory: ${emojisDir}`);
let converted = 0;
let skipped = 0;
let errors = 0;
try {
const emojis = await db`SELECT id, name, url FROM custom_emojis ORDER BY id`;
console.log(`[reconvert_emojis] Found ${emojis.length} emojis in DB.\n`);
for (const emoji of emojis) {
const { id, name, url } = emoji;
// Only handle locally hosted emojis (not external URLs)
if (!url || !url.startsWith('/s/emojis/')) {
console.log(`[${id}] "${name}" — skipping (external URL: ${url})`);
skipped++;
continue;
}
const filename = path.basename(url);
const ext = path.extname(filename).toLowerCase().replace('.', '');
if (ext === 'webp') {
// Verify the file actually exists, then skip
const fullPath = path.join(emojisDir, filename);
try {
await fs.access(fullPath);
console.log(`[${id}] "${name}" — already WebP, skipping.`);
} catch {
console.warn(`[${id}] "${name}" — already WebP in DB but file missing: ${fullPath}`);
}
skipped++;
continue;
}
const originalPath = path.join(emojisDir, filename);
const webpFilename = filename.replace(/\.[^.]+$/, '.webp');
const webpPath = path.join(emojisDir, webpFilename);
const newUrl = `/s/emojis/${webpFilename}`;
try {
await fs.access(originalPath);
} catch {
console.warn(`[${id}] "${name}" — original file missing: ${originalPath}, skipping.`);
skipped++;
continue;
}
try {
console.log(`[${id}] "${name}" — converting ${ext.toUpperCase()} → WebP …`);
// -coalesce: handles animated GIFs properly
// -quality 80: lossy WebP — ~50-70% smaller than GIF, transparency preserved
await execFile('magick', [originalPath, '-coalesce', '-quality', '80', webpPath], { env: magickEnv });
// Verify output exists and is non-empty
const stat = await fs.stat(webpPath);
if (!stat || stat.size === 0) throw new Error('Output file is empty');
// Update DB
await db`UPDATE custom_emojis SET url = ${newUrl} WHERE id = ${id}`;
// Remove original
await fs.unlink(originalPath);
console.log(`[${id}] "${name}" — ✓ converted (${(stat.size / 1024).toFixed(1)} KB) → ${newUrl}`);
converted++;
} catch (err) {
console.error(`[${id}] "${name}" — ✗ FAILED: ${err.message}`);
// Clean up partial output if it exists
await fs.unlink(webpPath).catch(() => {});
errors++;
}
}
// Notify connected clients of updated emoji list
if (converted > 0) {
await db`NOTIFY emojis_updated, '{}'`;
console.log(`\n[reconvert_emojis] Notified clients of emoji update.`);
}
console.log(`\n[reconvert_emojis] Done.`);
console.log(` Converted : ${converted}`);
console.log(` Skipped : ${skipped}`);
console.log(` Errors : ${errors}`);
process.exit(errors > 0 ? 1 : 0);
} catch (err) {
console.error('[reconvert_emojis] Fatal error:', err);
process.exit(1);
}