const fs = require("fs-extra"); const path = require("path"); const { spawn } = require("child_process"); const ytSearch = require("yt-search"); const axios = require("axios"); const ytdlpPath = path.join(__dirname, "yt-dlp.exe"); module.exports = { config: { name: "video", aliases: ["play", "yt", "gaan"], version: "16.0.0", author: "protik shah", countDown: 15, role: 0, shortDescription: "search & download video (-v) or audio (-a) from youtube", category: "media", guide: { en: "{p}video -v \n{p}video -a \nThen reply with a number (1-5) to pick." } }, onStart: async function ({ api, event, args }) { const { threadID, messageID, senderID } = event; if (args.length === 0) { return api.sendMessage( "āŒ Please provide a song name.\n\nšŸ’” Usage:\n-v (video)\n-a (audio only)", threadID, messageID ); } let isAudioOnly = false; let query = args.join(" "); if (args[0] === "-a" || args[0] === "--audio") { isAudioOnly = true; query = args.slice(1).join(" "); } else if (args[0] === "-v" || args[0] === "--video") { isAudioOnly = false; query = args.slice(1).join(" "); } if (!query) return api.sendMessage("āŒ Please provide a song name.", threadID, messageID); api.setMessageReaction("ā³", messageID, () => {}, true); try { const searchResults = await ytSearch(query); if (!searchResults || !searchResults.videos || searchResults.videos.length === 0) { api.setMessageReaction("āŒ", messageID, () => {}, true); return api.sendMessage("āŒ No results found.", threadID, messageID); } const topResults = searchResults.videos.slice(0, 5); // Download thumbnails to temp cache so we can attach them const cacheDir = path.join(__dirname, "cache"); await fs.ensureDir(cacheDir); const thumbPaths = []; for (let i = 0; i < topResults.length; i++) { try { const thumbUrl = topResults[i].thumbnail; const thumbPath = path.join(cacheDir, `thumb_${Date.now()}_${i}.jpg`); const response = await axios.get(thumbUrl, { responseType: "arraybuffer" }); await fs.writeFile(thumbPath, response.data); thumbPaths.push(thumbPath); } catch (e) { console.error("Thumbnail download error:", e.message); } } let listText = `šŸ” Results for: ${query}\n\n`; topResults.forEach((v, i) => { listText += `${i + 1}. ${v.title}\nā±ļø ${v.timestamp} | šŸ‘¤ ${v.author.name}\n\n`; }); listText += `šŸ‘‰ Reply with a number (1-${topResults.length}) to download as ${isAudioOnly ? "AUDIO" : "VIDEO"}.`; const attachments = thumbPaths.map(p => fs.createReadStream(p)); api.sendMessage( { body: listText, attachment: attachments.length > 0 ? attachments : undefined }, threadID, (err, info) => { // Clean up thumbnail files after sending thumbPaths.forEach(p => { if (fs.existsSync(p)) fs.unlinkSync(p); }); if (err) { console.error("Send list error:", err); return; } // Register the reply listener global.GoatBot.onReply.set(info.messageID, { commandName: this.config.name, messageID: info.messageID, author: senderID, results: topResults, isAudioOnly: isAudioOnly }); }, messageID ); api.setMessageReaction("āœ…", messageID, () => {}, true); } catch (err) { console.error("Video Search Error:", err); api.setMessageReaction("āŒ", messageID, () => {}, true); return api.sendMessage(`āŒ Error: ${err.message || "Something went wrong"}`, threadID, messageID); } }, onReply: async function ({ api, event, Reply }) { const { threadID, messageID, senderID, body } = event; // Only the person who searched can pick if (senderID !== Reply.author) return; const choice = parseInt(body.trim()); if (isNaN(choice) || choice < 1 || choice > Reply.results.length) { return api.sendMessage(`āŒ Please reply with a number between 1 and ${Reply.results.length}.`, threadID, messageID); } const selectedVideo = Reply.results[choice - 1]; const videoUrl = selectedVideo.url; const title = selectedVideo.title; const isAudioOnly = Reply.isAudioOnly; api.setMessageReaction("ā³", messageID, () => {}, true); api.sendMessage(`ā¬‡ļø Downloading ${isAudioOnly ? "audio" : "video"}: ${title}`, threadID); const cacheDir = path.join(__dirname, "cache"); await fs.ensureDir(cacheDir); const outputTemplate = path.join(cacheDir, `yt_${Date.now()}.%(ext)s`); const ytArgs = [ videoUrl, "--no-playlist", "--ffmpeg-location", __dirname, "-o", outputTemplate, "--print", "after_move:filepath" ]; if (isAudioOnly) { ytArgs.push("-x", "--audio-format", "mp3"); } else { ytArgs.push( "-f", "bestvideo[height<=480]+bestaudio/best[height<=480]", "--merge-output-format", "mp4" ); } let finalPath = null; let errorOutput = ""; const proc = spawn(ytdlpPath, ytArgs); proc.stdout.on("data", (data) => { const text = data.toString().trim(); if (text) finalPath = text; }); proc.stderr.on("data", (data) => { errorOutput += data.toString(); }); proc.on("close", async (code) => { try { if (code !== 0 || !finalPath || !fs.existsSync(finalPath)) { console.error("yt-dlp error output:", errorOutput); api.setMessageReaction("āŒ", messageID, () => {}, true); return api.sendMessage("āŒ Error: ঔাউনলোঔ করতে ą¦¬ą§ą¦Æą¦°ą§ą¦„ ą¦¹ą¦Æą¦¼ą§‡ą¦›ą§‡ą„¤", threadID); } const fileSize = (await fs.stat(finalPath)).size; if (fileSize > 25 * 1024 * 1024) { await fs.unlink(finalPath); api.setMessageReaction("šŸ“", messageID, () => {}, true); return api.sendMessage("āŒ File too large (>25MB). Try a shorter video.", threadID); } const attachment = fs.createReadStream(finalPath); const msg = isAudioOnly ? `šŸŽ»āœ“ your Searching music āœ”\nšŸŽ·ā˜žĀ«ā® ${title}` : `šŸŽ»āœ“ your Searching video āœ”\nšŸŽ·ā˜žĀ«ā® ${title}`; await api.sendMessage({ body: msg, attachment: attachment }, threadID, (err) => { if (err) console.error("Send error:", err); if (fs.existsSync(finalPath)) fs.unlinkSync(finalPath); }); api.setMessageReaction("āœ…", messageID, () => {}, true); } catch (err) { console.error("Reply Download Error:", err); api.setMessageReaction("āŒ", messageID, () => {}, true); return api.sendMessage(`āŒ Error: ${err.message || "Something went wrong"}`, threadID); } }); } };