From 623300ea4a24620ee4a65a1d4aa9fa2e81bce592 Mon Sep 17 00:00:00 2001 From: st Date: Mon, 13 Jul 2026 02:28:51 -0500 Subject: [PATCH] added snippet comband --- src/commands/music/snippet.js | 245 ++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 src/commands/music/snippet.js diff --git a/src/commands/music/snippet.js b/src/commands/music/snippet.js new file mode 100644 index 0000000..1c65027 --- /dev/null +++ b/src/commands/music/snippet.js @@ -0,0 +1,245 @@ +'use strict'; + +const fs = require('fs'); +const fsp = require('fs/promises'); +const os = require('os'); +const path = require('path'); +const { spawn } = require('child_process'); +const { pipeline } = require('stream/promises'); +const { Readable } = require('stream'); + +function isMp3Attachment(attachment) { + const name = (attachment.name || '').toLowerCase(); + const contentType = (attachment.contentType || '').toLowerCase(); + + return name.endsWith('.mp3') || contentType.startsWith('audio/'); +} + +function isVideoAttachment(attachment) { + const name = (attachment.name || '').toLowerCase(); + const contentType = (attachment.contentType || '').toLowerCase(); + + return ( + name.endsWith('.mp4') || + name.endsWith('.mov') || + name.endsWith('.mkv') || + name.endsWith('.webm') || + name.endsWith('.m4v') || + contentType.startsWith('video/') + ); +} + +function runProcess(command, args) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { stdio: ['ignore', 'ignore', 'pipe'] }); + let stderr = ''; + + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(); + return; + } + + reject(new Error(stderr || `${command} exited with code ${code}`)); + }); + }); +} + +async function downloadAttachment(attachment, filePath) { + const response = await fetch(attachment.url); + + if (!response.ok || !response.body) { + throw new Error(`failed to download ${attachment.name}`); + } + + await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(filePath)); +} + +async function getDuration(filePath) { + const probe = spawn('ffprobe', [ + '-v', 'error', + '-show_entries', 'format=duration', + '-of', 'default=noprint_wrappers=1:nokey=1', + filePath, + ], { stdio: ['ignore', 'pipe', 'pipe'] }); + + let stdout = ''; + let stderr = ''; + + probe.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + + probe.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + const exitCode = await new Promise((resolve, reject) => { + probe.on('error', reject); + probe.on('close', resolve); + }); + + if (exitCode !== 0) { + throw new Error(stderr || 'failed to read media duration'); + } + + const duration = Number.parseFloat(stdout.trim()); + if (!Number.isFinite(duration) || duration <= 0) { + throw new Error('invalid media duration'); + } + + return duration; +} + +function buildEmbed(client, message, description, fields = []) { + const embed = { + color: 0x8aa2b3, + title: '', + author: { + name: message.author.username, + icon_url: message.author.displayAvatarURL(), + }, + description, + footer: { + text: `twitter @ ${message.guild?.name || 'dms'}`, + icon_url: client.user.displayAvatarURL(), + }, + timestamp: new Date(), + }; + + if (fields.length > 0) { + embed.fields = fields; + } + + return embed; +} + +module.exports = { + name: 'snippet', + description: 'mix a random mp3 segment with a random video segment and export a clipped video', + aliases: ['mix', 'clip'], + usage: 'snippet [mp3 attachment] [video attachment]', + + async execute(client, message, args = []) { + const requestedLength = Number.parseInt(args[0], 10); + + if (!Number.isInteger(requestedLength) || requestedLength <= 0) { + return message.reply({ + content: null, + embeds: [buildEmbed( + client, + message, + '<:no:1481811955592466492> provide a clip length in seconds, then attach one mp3 and one video file.', + )], + }); + } + + const attachments = [...message.attachments.values()]; + const mp3Attachment = attachments.find(isMp3Attachment); + const videoAttachment = attachments.find(isVideoAttachment); + + if (!mp3Attachment || !videoAttachment) { + return message.reply({ + content: null, + embeds: [buildEmbed( + client, + message, + '<:no:1481811955592466492> attach exactly one mp3 file and one video file to use this command.', + )], + }); + } + + const sent = await message.reply({ + content: null, + embeds: [buildEmbed( + client, + message, + ' building your clip...', + )], + }); + + const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'twitter-snippet-')); + const mp3Path = path.join(tempDir, mp3Attachment.name || 'input.mp3'); + const videoPath = path.join(tempDir, videoAttachment.name || 'input.mp4'); + const outputPath = path.join(tempDir, 'snippet.mp4'); + + try { + await Promise.all([ + downloadAttachment(mp3Attachment, mp3Path), + downloadAttachment(videoAttachment, videoPath), + ]); + + const mp3Duration = await getDuration(mp3Path); + const videoDuration = await getDuration(videoPath); + + if (requestedLength > mp3Duration || requestedLength > videoDuration) { + throw new Error('requested length is longer than one of the attached files'); + } + + const clipLength = requestedLength; + + const videoStart = videoDuration === clipLength + ? 0 + : Math.random() * (videoDuration - clipLength); + const audioStart = mp3Duration === clipLength + ? 0 + : Math.random() * (mp3Duration - clipLength); + + await runProcess('ffmpeg', [ + '-y', + '-ss', videoStart.toFixed(3), + '-i', videoPath, + '-ss', audioStart.toFixed(3), + '-i', mp3Path, + '-t', clipLength.toFixed(3), + '-map', '0:v:0', + '-map', '1:a:0', + '-c:v', 'libx264', + '-preset', 'veryfast', + '-crf', '20', + '-c:a', 'aac', + '-b:a', '192k', + '-pix_fmt', 'yuv420p', + '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', + '-movflags', '+faststart', + outputPath, + ]); + + const finalEmbed = buildEmbed( + client, + message, + '<:check:1481811973447356496> clip finished successfully.', + [ + { name: 'mp3 source', value: `**${mp3Attachment.name || 'unknown'}**`, inline: true }, + { name: 'video source', value: `**${videoAttachment.name || 'unknown'}**`, inline: true }, + { name: 'clip length', value: `**${clipLength.toFixed(2)}s**`, inline: true }, + ], + ); + + await sent.edit({ + content: null, + embeds: [finalEmbed], + files: [{ attachment: outputPath, name: 'snippet.mp4' }], + }); + } catch (err) { + await sent.edit({ + content: null, + embeds: [buildEmbed( + client, + message, + '<:no:1481811955592466492> failed to build the clip.', + [ + { name: 'reason', value: `**${err.message || 'unknown error'}**` }, + ], + )], + }).catch(() => null); + } finally { + await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => null); + } + }, +};