Skip to content

Instantly share code, notes, and snippets.

@weskerty
Last active October 18, 2025 08:14
Show Gist options
  • Select an option

  • Save weskerty/9789dee090b6c3f0cdf99e756169d0b3 to your computer and use it in GitHub Desktop.

Select an option

Save weskerty/9789dee090b6c3f0cdf99e756169d0b3 to your computer and use it in GitHub Desktop.
const fs = require('fs').promises;
const path = require('path');
const { promisify } = require('util');
const { exec: execCallback } = require('child_process');
const { bot } = require('../lib');
const exec = promisify(execCallback);
const utils = {
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
formatSize(sizeInBytes) {
if (!sizeInBytes || isNaN(sizeInBytes)) return "Desconocido";
const mb = sizeInBytes / (1024 * 1024);
return mb < 1024 ? `${mb.toFixed(2)} MB` : `${(mb / 1024).toFixed(2)} GB`;
},
formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0";
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
};
class MessageUtils {
static getVideoDetails(quotedMessage) {
try {
const fullMessage = quotedMessage.message?.message || quotedMessage;
if (fullMessage?.videoMessage) {
return {
isVideo: true,
mimetype: fullMessage.videoMessage.mimetype || 'video/mp4',
fileLength: fullMessage.videoMessage.fileLength ? parseInt(fullMessage.videoMessage.fileLength) : null
};
}
if (fullMessage?.documentMessage) {
const mimetype = fullMessage.documentMessage.mimetype || '';
return {
isVideo: mimetype.startsWith('video/'),
mimetype: fullMessage.documentMessage.mimetype,
fileLength: fullMessage.documentMessage.fileLength ? parseInt(fullMessage.documentMessage.fileLength) : null
};
}
return { isVideo: false };
} catch (error) {
console.error('VIDEOPLUGIN: Error getting video details:', error);
return { isVideo: false };
}
}
}
class VideoProcessor {
constructor() {
this.config = null;
}
setContext(ctx) {
this.config = {
tempDir: ctx.TEMP_DOWNLOAD_DIR || path.join(process.cwd(), 'tmp'),
maxFileSize: (parseInt(ctx.MAX_UPLOAD, 10) * 1048576) || 1500000000,
maxTotalDuration: 900,
outputFormat: 'mp4',
segmentDuration: 30,
sendDelay: 1000
};
}
async safeExecute(command) {
try {
return await exec(command);
} catch (error) {
console.error(`VIDEOPLUGIN: Command failed: ${command}`);
console.error(`VIDEOPLUGIN: Error: ${error.message}`);
throw error;
}
}
async isFFmpegAvailable() {
try {
await exec('ffmpeg -version');
return true;
} catch {
return false;
}
}
async isVaapiAvailable() {
try {
const command = 'ffmpeg -hide_banner -hwaccels';
const result = await exec(command);
return result.stdout.includes('vaapi');
} catch {
return false;
}
}
async getVideoDuration(filePath) {
try {
const command = `ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${filePath}"`;
const result = await this.safeExecute(command);
return parseFloat(result.stdout.trim());
} catch (error) {
console.error(`VIDEOPLUGIN: Error getting duration: ${error.message}`);
return null;
}
}
async getVideoInfo(filePath) {
try {
const command = `ffprobe -i "${filePath}"`;
const result = await exec(command);
return result.stderr || result.stdout;
} catch (error) {
return error.stderr || error.stdout || error.message;
}
}
async splitVideo(inputPath, outputDir, segmentDuration = 30) {
const outputPattern = path.join(outputDir, 'segment_%03d.mp4');
const vaapiAvailable = await this.isVaapiAvailable();
let command;
if (vaapiAvailable) {
command = [
'ffmpeg',
'-hwaccel', 'vaapi',
'-hwaccel_device', '/dev/dri/renderD128',
'-hwaccel_output_format', 'vaapi',
'-i', `"${inputPath}"`,
'-vf', 'scale_vaapi=format=nv12',
'-c:v', 'h264_vaapi',
'-c:a', 'aac',
'-b:a', '128k',
'-map', '0',
'-segment_time', segmentDuration.toString(),
'-f', 'segment',
'-reset_timestamps', '1',
'-avoid_negative_ts', 'make_zero',
`"${outputPattern}"`
].join(' ');
} else {
command = [
'ffmpeg',
'-i', `"${inputPath}"`,
'-c:v', 'libx264',
'-preset', 'fast',
'-c:a', 'aac',
'-b:a', '128k',
'-map', '0',
'-segment_time', segmentDuration.toString(),
'-f', 'segment',
'-reset_timestamps', '1',
'-avoid_negative_ts', 'make_zero',
`"${outputPattern}"`
].join(' ');
}
try {
await this.safeExecute(command);
} catch (error) {
if (vaapiAvailable) {
const fallbackCommand = [
'ffmpeg',
'-i', `"${inputPath}"`,
'-c:v', 'libx264',
'-preset', 'fast',
'-c:a', 'aac',
'-b:a', '128k',
'-map', '0',
'-segment_time', segmentDuration.toString(),
'-f', 'segment',
'-reset_timestamps', '1',
'-avoid_negative_ts', 'make_zero',
`"${outputPattern}"`
].join(' ');
await this.safeExecute(fallbackCommand);
} else {
throw error;
}
}
const files = await fs.readdir(outputDir);
return files
.filter(file => file.startsWith('segment_') && file.endsWith('.mp4'))
.sort()
.map(file => path.join(outputDir, file));
}
async compressVideo(inputPath, outputPath) {
const vaapiAvailable = await this.isVaapiAvailable();
let command;
if (vaapiAvailable) {
command = [
'ffmpeg',
'-hwaccel', 'vaapi',
'-hwaccel_device', '/dev/dri/renderD128',
'-hwaccel_output_format', 'vaapi',
'-i', `"${inputPath}"`,
'-vf', 'scale_vaapi=format=nv12',
'-c:v', 'h264_vaapi',
'-b:v', '512k',
'-an',
`"${outputPath}"`
].join(' ');
} else {
command = [
'ffmpeg',
'-i', `"${inputPath}"`,
'-c:v', 'libx264',
'-preset', 'fast',
'-crf', '28',
'-an',
`"${outputPath}"`
].join(' ');
}
try {
await this.safeExecute(command);
} catch (error) {
if (vaapiAvailable) {
const fallbackCommand = [
'ffmpeg',
'-i', `"${inputPath}"`,
'-c:v', 'libx264',
'-preset', 'fast',
'-crf', '28',
'-an',
`"${outputPath}"`
].join(' ');
await this.safeExecute(fallbackCommand);
} else {
throw error;
}
}
}
}
const videoProcessor = new VideoProcessor();
bot(
{
pattern: '30s ?(.*)',
fromMe: true,
desc: 'Split quoted video into 30-second segments',
type: 'media',
},
async (message, match, ctx) => {
videoProcessor.setContext(ctx);
const reply = async (text, isError = false) => {
await message.send(`${isError ? '❌' : '✅'} ${text}`, { quoted: message.data });
};
try {
const quotedMessage = message.reply_message;
if (!quotedMessage) {
return await reply('Debes citar un video para dividir en partes de 30 segundos', true);
}
const videoDetails = MessageUtils.getVideoDetails(quotedMessage);
if (!videoDetails.isVideo) {
return await reply('El archivo citado no es video', true);
}
if (videoDetails.fileLength && videoDetails.fileLength > videoProcessor.config.maxFileSize) {
return await reply(
`Video demasiado grande (${utils.formatSize(videoDetails.fileLength)}). Máximo: ${utils.formatSize(videoProcessor.config.maxFileSize)}`,
true
);
}
if (!await videoProcessor.isFFmpegAvailable()) {
return await reply('FFmpeg no instalado', true);
}
const sessionId = `split30s_${Date.now()}`;
const tempDir = path.join(videoProcessor.config.tempDir, sessionId);
try {
await fs.mkdir(tempDir, { recursive: true });
const mediaBuffer = await quotedMessage.downloadMediaMessage();
if (!mediaBuffer) {
return await reply('No se pudo descargar el video citado', true);
}
const tempVideoPath = path.join(tempDir, 'input_video.mp4');
await fs.writeFile(tempVideoPath, mediaBuffer);
const duration = await videoProcessor.getVideoDuration(tempVideoPath);
if (duration && duration > videoProcessor.config.maxTotalDuration) {
return await reply(
`Video demasiado largo (${utils.formatTime(duration)}). Máximo permitido: ${utils.formatTime(videoProcessor.config.maxTotalDuration)}`,
true
);
}
const segmentPaths = await videoProcessor.splitVideo(tempVideoPath, tempDir, videoProcessor.config.segmentDuration);
if (segmentPaths.length === 0) {
return await reply('No se pudieron generar segmentos', true);
}
for (let i = 0; i < segmentPaths.length; i++) {
const segmentPath = segmentPaths[i];
const segmentNumber = i + 1;
try {
const segmentBuffer = await fs.readFile(segmentPath);
const fileName = `parte_${segmentNumber.toString().padStart(2, '0')}_de_${segmentPaths.length}.mp4`;
const startTime = i * videoProcessor.config.segmentDuration;
const endTime = Math.min((i + 1) * videoProcessor.config.segmentDuration, duration || (i + 1) * videoProcessor.config.segmentDuration);
await message.send(
segmentBuffer,
{
fileName: fileName,
mimetype: 'video/mp4',
quoted: message.data,
caption: `📹 Parte ${segmentNumber}/${segmentPaths.length}\n⏱️ ${utils.formatTime(startTime)} - ${utils.formatTime(endTime)}`
},
'video'
);
if (i < segmentPaths.length - 1) {
await utils.delay(videoProcessor.config.sendDelay);
}
} catch (sendError) {
console.error(`VIDEOPLUGIN: Error sending segment ${segmentNumber}: ${sendError.message}`);
await reply(`Error enviando parte ${segmentNumber}: ${sendError.message}`, true);
}
}
} catch (error) {
console.error(`VIDEOPLUGIN: Error processing video: ${error.message}`);
await reply(`Error procesando video: ${error.message}`, true);
} finally {
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
} catch (error) {
console.error('VIDEOPLUGIN: error:', error);
await reply(`Error: ${error.message}`, true);
}
}
);
bot(
{
pattern: 'metainfo ?(.*)',
fromMe: true,
desc: 'Get video metadata using ffprobe',
type: 'media',
},
async (message, match, ctx) => {
videoProcessor.setContext(ctx);
const reply = async (text, isError = false) => {
await message.send(`${isError ? '❌' : '✅'} ${text}`, { quoted: message.data });
};
try {
const quotedMessage = message.reply_message;
if (!quotedMessage) {
return await reply('Debes citar un video para obtener su metadata', true);
}
const videoDetails = MessageUtils.getVideoDetails(quotedMessage);
if (!videoDetails.isVideo) {
return await reply('El archivo citado no es video', true);
}
if (!await videoProcessor.isFFmpegAvailable()) {
return await reply('FFmpeg no instalado', true);
}
const sessionId = `metainfo_${Date.now()}`;
const tempDir = path.join(videoProcessor.config.tempDir, sessionId);
try {
await fs.mkdir(tempDir, { recursive: true });
const mediaBuffer = await quotedMessage.downloadMediaMessage();
if (!mediaBuffer) {
return await reply('No se pudo descargar el video citado', true);
}
const tempVideoPath = path.join(tempDir, 'video.mp4');
await fs.writeFile(tempVideoPath, mediaBuffer);
const info = await videoProcessor.getVideoInfo(tempVideoPath);
await message.send(`📊 *Metadata del video:*\n\`\`\`\n${info}\n\`\`\``, { quoted: message.data });
} catch (error) {
console.error(`VIDEOPLUGIN: Error getting metadata: ${error.message}`);
await reply(`Error obteniendo metadata: ${error.message}`, true);
} finally {
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
} catch (error) {
console.error('VIDEOPLUGIN: error:', error);
await reply(`Error: ${error.message}`, true);
}
}
);
bot(
{
pattern: 'lite ?(.*)',
fromMe: true,
desc: 'Compress video to 50% size, h264, no audio',
type: 'media',
},
async (message, match, ctx) => {
videoProcessor.setContext(ctx);
const reply = async (text, isError = false) => {
await message.send(`${isError ? '❌' : '✅'} ${text}`, { quoted: message.data });
};
try {
const quotedMessage = message.reply_message;
if (!quotedMessage) {
return await reply('Debes citar un video para comprimir', true);
}
const videoDetails = MessageUtils.getVideoDetails(quotedMessage);
if (!videoDetails.isVideo) {
return await reply('El archivo citado no es video', true);
}
if (videoDetails.fileLength && videoDetails.fileLength > videoProcessor.config.maxFileSize) {
return await reply(
`Video demasiado grande (${utils.formatSize(videoDetails.fileLength)}). Máximo: ${utils.formatSize(videoProcessor.config.maxFileSize)}`,
true
);
}
if (!await videoProcessor.isFFmpegAvailable()) {
return await reply('FFmpeg no instalado', true);
}
const sessionId = `lite_${Date.now()}`;
const tempDir = path.join(videoProcessor.config.tempDir, sessionId);
try {
await fs.mkdir(tempDir, { recursive: true });
const mediaBuffer = await quotedMessage.downloadMediaMessage();
if (!mediaBuffer) {
return await reply('No se pudo descargar el video citado', true);
}
const tempVideoPath = path.join(tempDir, 'input_video.mp4');
const outputVideoPath = path.join(tempDir, 'output_lite.mp4');
await fs.writeFile(tempVideoPath, mediaBuffer);
await videoProcessor.compressVideo(tempVideoPath, outputVideoPath);
const outputBuffer = await fs.readFile(outputVideoPath);
const outputStats = await fs.stat(outputVideoPath);
const originalSize = videoDetails.fileLength || mediaBuffer.length;
const compressedSize = outputStats.size;
const reduction = ((1 - (compressedSize / originalSize)) * 100).toFixed(2);
await message.send(
outputBuffer,
{
fileName: 'video_lite.mp4',
mimetype: 'video/mp4',
quoted: message.data,
caption: `🗜️ *Video comprimido*\n📦 Original: ${utils.formatSize(originalSize)}\n📉 Comprimido: ${utils.formatSize(compressedSize)}\n💾 Reducción: ${reduction}%`
},
'video'
);
} catch (error) {
console.error(`VIDEOPLUGIN: Error compressing video: ${error.message}`);
await reply(`Error comprimiendo video: ${error.message}`, true);
} finally {
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
} catch (error) {
console.error('VIDEOPLUGIN: error:', error);
await reply(`Error: ${error.message}`, true);
}
}
);
module.exports = { VideoProcessor };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment