// code from goatstore // api: https://store.agi.bd const axios = require("axios"); const yts = require("yt-search"); const fs = require("fs"); const path = require("path"); const CACHE_DIR = path.join(__dirname, "cache"); const VIDEO_API_BASE = "https://eryxenx.agi.bd/api/video"; // ⚠️ Audio API: jodi tomar API er audio endpoint onno hoy, shudhu eta change koro const AUDIO_API_BASE = ""; const MAX_RESULTS = 6; function ensureCache() { if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true }); } function tempFilePath(ext) { ensureCache(); return path.join(CACHE_DIR, `media_${Date.now()}_${Math.floor(Math.random() * 1e4)}.${ext}`); } async function fetchMediaInfo(videoUrl, type) { const base = type === "audio" ? AUDIO_API_BASE : VIDEO_API_BASE; const infoRes = await axios.get(base, { params: { url: videoUrl }, timeout: 60000 }); const data = infoRes.data; if (!data?.success || !data?.downloadUrl) { throw new Error(data?.error || "downloadUrl paoa jayni API response e"); } return data; } async function streamDownloadToFile(dlUrl, filePath, type) { ensureCache(); const response = await axios.get(dlUrl, { responseType: "stream", timeout: 300000, maxContentLength: Infinity, maxBodyLength: Infinity, headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } }); const contentType = response.headers["content-type"] || ""; const isValid = contentType.includes("video") || contentType.includes("audio") || contentType.includes("octet-stream") || contentType.includes("mpeg") || contentType.includes("mp4"); if (!isValid) { let bodyText = ""; try { const chunks = []; for await (const chunk of response.data) { chunks.push(chunk); if (Buffer.concat(chunks).length > 2000) break; } bodyText = Buffer.concat(chunks).toString("utf-8").slice(0, 500); } catch (_) {} throw new Error(`Invalid content received from downloadUrl (type: ${contentType})` + (bodyText ? ` — upstream said: "${bodyText.trim()}"` : "")); } const writer = fs.createWriteStream(filePath); await new Promise((resolve, reject) => { response.data.pipe(writer); let failed = false; const onError = (err) => { if (failed) return; failed = true; writer.close(); fs.unlink(filePath, () => {}); reject(err); }; response.data.on("error", onError); writer.on("error", onError); writer.on("close", () => { if (!failed) resolve(); }); }); const stats = fs.statSync(filePath); if (stats.size < 1024) { fs.unlink(filePath, () => {}); throw new Error(`Downloaded file too small (${stats.size} bytes) — corrupt ba failed download`); } } function extractApiErrorMessage(err) { const raw = err.response?.data; if (raw && typeof raw === "object" && !Buffer.isBuffer(raw)) { if (raw.error) return raw.error; if (raw.message) return raw.message; } if (raw) { try { const text = Buffer.isBuffer(raw) ? raw.toString("utf-8") : String(raw); const parsed = JSON.parse(text); if (parsed?.error) return parsed.error; if (parsed?.message) return parsed.message; } catch (_) {} } return err.message; } async function sendWithRetry(message, msg, retries = 2) { for (let i = 0; i <= retries; i++) { try { return await message.reply(msg); } catch (err) { const is408 = err?.error === 408 || String(err?.message || err).includes("408"); if (is408 && i < retries) { await new Promise(r => setTimeout(r, 2000)); continue; } throw err; } } } // thumbnail gulo download kore stream banay (fail hole skip) async function getThumbStreams(videos) { ensureCache(); const files = []; const streams = []; await Promise.all(videos.map(async (v, i) => { try { const url = v.thumbnail || v.image; if (!url) return; const res = await axios.get(url, { responseType: "arraybuffer", timeout: 15000 }); const p = path.join(CACHE_DIR, `thumb_${Date.now()}_${i}_${Math.floor(Math.random() * 1e4)}.jpg`); fs.writeFileSync(p, res.data); files[i] = p; } catch (_) {} })); for (const p of files) { if (p) streams.push(fs.createReadStream(p)); } return { streams, files: files.filter(Boolean) }; } function cleanFiles(files) { for (const f of files) fs.unlink(f, () => {}); } module.exports = { config: { name: "youtube", aliases: ["ytb","ytvideo"], version: "3.0.0", author: "EryXenX", countDown: 5, role: 0, shortDescription: { en: "Search YouTube and pick video/audio by number" }, longDescription: { en: "Search YouTube, get a numbered list, then reply with a number to download the video or audio." }, category: "media", guide: { en: "{pn} → video list\n{pn} -a → audio (mp3) list" } }, onStart: async function ({ message, args, event, api, commandName }) { let type = "video"; if (["-a", "-audio", "audio"].includes((args[0] || "").toLowerCase())) { type = "audio"; args.shift(); } else if (["-v", "-video", "video"].includes((args[0] || "").toLowerCase())) { args.shift(); } const query = args.join(" ").trim(); if (!query) { return message.reply("Please provide a name.\n\n🎬 Video: video \n🎵 Audio: video -a "); } api.setMessageReaction("⏳", event.messageID); let thumbFiles = []; try { const search = await yts(query); const videos = (search.videos || []).slice(0, MAX_RESULTS); if (!videos.length) { api.setMessageReaction("❌", event.messageID); return message.reply("No results found for your query."); } const icon = type === "audio" ? "🎵" : "🎬"; let body = `${icon} ${type === "audio" ? "AUDIO" : "VIDEO"} SEARCH RESULTS\n━━━━━━━━━━━━━━━━━━\n`; videos.forEach((v, i) => { body += `\n${i + 1}. ${v.title}\n ⏱ ${v.timestamp || "N/A"} | 👤 ${v.author?.name || "Unknown"}\n`; }); body += `\n━━━━━━━━━━━━━━━━━━\n👉 Reply with a number (1-${videos.length}) to download.`; const { streams, files } = await getThumbStreams(videos); thumbFiles = files; const payload = streams.length ? { body, attachment: streams } : { body }; message.reply(payload, (err, info) => { cleanFiles(thumbFiles); if (err || !info) return; global.GoatBot.onReply.set(info.messageID, { commandName, messageID: info.messageID, author: event.senderID, type, videos: videos.map(v => ({ url: v.url, title: v.title })) }); }); api.setMessageReaction("✅", event.messageID); } catch (e) { cleanFiles(thumbFiles); console.error("[VIDEO SEARCH ERROR]:", e?.response?.data || e.message || e); api.setMessageReaction("❌", event.messageID); message.reply("Search failed: " + extractApiErrorMessage(e)); } }, onReply: async function ({ message, event, api, Reply }) { const { author, type, videos, messageID: listMsgID } = Reply; // shudhu je search koreche se-i number dite parbe if (event.senderID !== author) return; const choice = parseInt((event.body || "").trim(), 10); if (isNaN(choice) || choice < 1 || choice > videos.length) { return message.reply(`❗ Please reply with a valid number between 1 and ${videos.length}.`); } const picked = videos[choice - 1]; // list message ta unsend + reply handler remove try { global.GoatBot.onReply.delete(listMsgID); } catch (_) {} try { await message.unsend(listMsgID); } catch (_) {} api.setMessageReaction("⏳", event.messageID); let file; try { const info = await fetchMediaInfo(picked.url, type); file = tempFilePath(type === "audio" ? "mp3" : "mp4"); await streamDownloadToFile(info.downloadUrl, file, type); await sendWithRetry(message, { body: `${type === "audio" ? "🎵" : "🎬"} ${info.title || picked.title}`, attachment: fs.createReadStream(file) }); api.setMessageReaction("✅", event.messageID); } catch (e) { console.error("[VIDEO DOWNLOAD ERROR]:", e?.response?.data || e.message || e); api.setMessageReaction("❌", event.messageID); message.reply("Download failed: " + extractApiErrorMessage(e)); } finally { if (file) fs.unlink(file, () => {}); } } };