reconvert all emojis to webp
This commit is contained in:
119
scripts/reconvert_emojis.mjs
Normal file
119
scripts/reconvert_emojis.mjs
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/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);
|
||||
}
|
||||
Reference in New Issue
Block a user