Skip to content

Instantly share code, notes, and snippets.

@weskerty
Last active January 9, 2026 15:21
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.ctx = null;
}
setContext(ctx) {
this.ctx = ctx;
this.config = {
tempDir: ctx.TEMP_DOWNLOAD_DIR || path.join(process.cwd(), 'tmp'),
maxFileSize: (parseInt(ctx.MAX_UPLOAD, 10) * 1048576) || 1500000000,
maxTotalDuration: parseInt(ctx.VIDEO_MAX_DURATION, 10) || 900,
outputFormat: 'mp4',
segmentDuration: parseInt(ctx.VIDEO_SEGMENT_DURATION, 10) || 30,
sendDelay: parseInt(ctx.VIDEO_SEND_DELAY, 10) || 1000,
compressionRatio: parseFloat(ctx.VIDEO_COMPRESSION_RATIO) || 0.5
};
}
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 stats = await fs.stat(inputPath);
const fileSizeBytes = stats.size;
const duration = await this.getVideoDuration(inputPath);
if (!duration || duration <= 0) {
throw new Error('No se pudo obtener duración del video');
}
const targetSizeBytes = fileSizeBytes * this.config.compressionRatio;
const targetBitrate = Math.floor((targetSizeBytes * 8) / duration);
const videoBitrate = Math.floor(targetBitrate * 0.9);
const audioBitrate = Math.min(128000, Math.floor(targetBitrate * 0.1));
const probeCmd = `ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 "${inputPath}"`;
const { stdout } = await exec(probeCmd);
const [w, h] = stdout.trim().split(',').map(Number);
let scale = '';
if (w > 1280 || h > 720) {
scale = '-vf scale=1280:-2:flags=lanczos';
} else if (w > 854 || h > 480) {
scale = '-vf scale=854:-2:flags=lanczos';
}
const vaapiAvailable = await this.isVaapiAvailable();
let cmd;
if (vaapiAvailable) {
const scaleVaapi = scale ? `-vf scale_vaapi=${scale.includes('1280') ? 'w=1280:h=-2' : 'w=854:h=-2'}` : '';
cmd = [
'ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128',
`-i "${inputPath}"`,
scaleVaapi,
`-c:v h264_vaapi -b:v ${videoBitrate}`,
`-c:a aac -b:a ${Math.floor(audioBitrate/1000)}k -ar 44100`,
`-movflags +faststart "${outputPath}"`
].join(' ');
} else {
cmd = [
`ffmpeg -i "${inputPath}"`,
scale,
`-c:v libx264 -preset medium -crf 23 -maxrate ${videoBitrate} -bufsize ${videoBitrate * 2}`,
`-c:a aac -b:a ${Math.floor(audioBitrate/1000)}k -ar 44100`,
`-movflags +faststart "${outputPath}"`
].join(' ');
}
try {
await this.safeExecute(cmd);
} catch (error) {
if (vaapiAvailable) {
const fallback = [
`ffmpeg -i "${inputPath}"`,
scale,
`-c:v libx264 -preset medium -crf 23 -maxrate ${videoBitrate} -bufsize ${videoBitrate * 2}`,
`-c:a aac -b:a ${Math.floor(audioBitrate/1000)}k -ar 44100`,
`-movflags +faststart "${outputPath}"`
].join(' ');
await this.safeExecute(fallback);
} else {
throw error;
}
}
}
}
const videoProcessor = new VideoProcessor();
bot(
{
pattern: '30s ?(.*)',
fromMe: true,
desc: 'Split quoted video into 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', 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 with 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