// code from goatstore // api: https://store.agi.bd const axios = require("axios"); const fs = require("fs"); const path = require("path"); const { pipeline } = require("stream/promises"); const cacheDir = path.join(__dirname, "cache"); if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true }); const MAX_SIZE = 25 * 1024 * 1024; // Messenger attachment limit const API_JSON = "https://raw.githubusercontent.com/MR-MAHABUB-004/MAHABUB-BOT-STORAGE/refs/heads/main/APIURL.json"; const DL_HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36", Accept: "*/*" }; let API_CACHE = null; async function getApiConfig() { if (API_CACHE) return API_CACHE; const res = await axios.get(API_JSON, { timeout: 15000 }); if (!res.data || typeof res.data !== "object") throw new Error("API config invalid"); API_CACHE = res.data; return API_CACHE; } // Audio download API: cfg.ytmp3 -> BASE/ytmp3v4?url= async function getBaseApi() { const cfg = await getApiConfig(); if (!cfg.ytmp3) throw new Error("ytmp3 API URL not found"); return String(cfg.ytmp3).replace(/\/+$/, ""); } // Search API: cfg.api (called as BASE/mahabub/ytsearch?q=) async function getSearchApi() { const cfg = await getApiConfig(); if (!cfg.api) throw new Error("search API URL not found"); return String(cfg.api).replace(/\/+$/, ""); } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // Safe error-body printer (stream bodies are skipped) const errBody = (err) => { const d = err?.response?.data; if (!d || typeof d.pipe === "function") return ""; try { return typeof d === "string" ? d.slice(0, 300) : JSON.stringify(d).slice(0, 300); } catch { return ""; } }; // Call ytmp3v4 API with retry -> returns the full response object // Response: { status, title, author, videoId, filesize, download_url, expires_in, ... } async function fetchMp3Link(MP3_API, youtubeUrl) { const apiUrl = `${MP3_API}/ytmp3v4?url=${encodeURIComponent(youtubeUrl)}`; let lastErr = null; for (let attempt = 1; attempt <= 3; attempt++) { try { const { data } = await axios.get(apiUrl, { timeout: 120000 }); if (data?.status === "success" && /^https?:\/\//i.test(data?.download_url || "")) { return data; } lastErr = new Error(data?.message || "API returned no download link"); console.error("[SING] Invalid API response:", JSON.stringify(data)?.slice(0, 500)); } catch (e) { lastErr = e; console.error( `[SING] ytmp3v4 attempt ${attempt} failed:`, e?.response?.status || e?.code || "", e?.message ); } if (attempt < 3) await sleep(attempt * 3000); } throw lastErr; } module.exports = { config: { name: "sing", version: "5.5", author: "@๐Œ๐‘แญ„๏น… ๐Œ๐€๐‡๐€๐๐”๐๏น… ใƒก๊ชœ", countDown: 5, role: 0, longDescription: { en: "๐ŸŽถ Search song โ†’ reply number โ†’ auto download โ†’ auto unsend โ†’ auto delete" }, category: "media", guide: { en: "{pn} \nExample: sing toh phir aao" } }, // ============================================================ // SONG SEARCH // ============================================================ onStart: async ({ api, args, event }) => { if (!args.length) { return api.sendMessage( "โŒ **Song name dao na!**\n\n๐Ÿ“Œ Example:\nโžค `sing toh phir aao`", event.threadID, event.messageID ); } api.setMessageReaction("โณ", event.messageID, () => {}, true); try { const BASE = await getSearchApi(); const query = encodeURIComponent(args.join(" ")); const response = await axios.get(`${BASE}/mahabub/ytsearch?q=${query}`, { timeout: 30000 }); const data = response.data; console.log("[SING] Search response:", JSON.stringify(data)?.slice(0, 1500)); const pick = (d) => [ d?.videos, d?.data?.videos, d?.results, d?.data?.results, d?.data, d?.result, d?.data?.data, d?.items, Array.isArray(d) ? d : null ].find((x) => Array.isArray(x) && x.length > 0); const rawList = pick(data); if (!rawList) { api.setMessageReaction("โŒ", event.messageID, () => {}, true); return api.sendMessage( "๐Ÿšซ **No results found!**\nTry another keyword ๐ŸŽง", event.threadID ); } const results = rawList.slice(0, 10).map((it) => { const url = it.url || it.link || ""; const idFromUrl = (url.match(/[?&]v=([\w-]{11})/) || url.match(/youtu\.be\/([\w-]{11})/) || [])[1]; return { ...it, videoId: it.videoId || it.video_id || it.id || idFromUrl, title: it.title || it.name || "Unknown", thumbnail: it.thumbnail || it.thumb || it.image }; }); let body = "๐ŸŽงโœจ **SONG SEARCH RESULTS** โœจ๐ŸŽง\n" + "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n" + `๐Ÿ”Ž **Query:** ${data.query || args.join(" ")}\n\n`; results.forEach((item, i) => { const meta = [item.duration && `โฑ ${item.duration}`, item.author] .filter(Boolean) .join(" โ€ข "); body += `๐ŸŽต **${i + 1}.** ${item.title}\n`; if (meta) body += ` ${meta}\n`; }); const thumbs = await Promise.all( results.map(async (item) => { const thumbnail = item?.thumbnails?.high || item?.thumbnails?.medium || item?.thumbnails?.default || item?.thumbnail; if (!thumbnail) return null; try { const image = await axios.get(thumbnail, { responseType: "stream", timeout: 15000 }); return image.data; } catch (err) { return null; } }) ); const attachments = thumbs.filter(Boolean); body += "\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n" + `๐Ÿ“ **Reply with a number (1-${results.length})**\n` + "โฌ‡๏ธ To download audio ๐ŸŽถ"; api.sendMessage( { body, attachment: attachments.length > 0 ? attachments : undefined }, event.threadID, (err, info) => { if (err) { console.error("Search message error:", err); return; } if (!info?.messageID) return; global.GoatBot.onReply.set(info.messageID, { commandName: "sing", author: event.senderID, results, searchMsgID: info.messageID }); }, event.messageID ); api.setMessageReaction("๐ŸŽถ", event.messageID, () => {}, true); } catch (err) { console.error("SONG SEARCH ERROR:", err?.response?.status, err?.message); api.setMessageReaction("โŒ", event.messageID, () => {}, true); api.sendMessage("โŒ **Search failed!**\nTry again later.", event.threadID); } }, // ============================================================ // REPLY HANDLER // ============================================================ onReply: async ({ api, event, Reply }) => { if (String(event.senderID) !== String(Reply.author)) return; const choice = parseInt(String(event.body || "").trim(), 10); if (Number.isNaN(choice) || choice < 1 || choice > Reply.results.length) { api.setMessageReaction("โŒ", event.messageID, () => {}, true); return api.sendMessage( "โš ๏ธ **Invalid number!**\n" + `Reply between 1-${Reply.results.length} ๐Ÿ”ข`, event.threadID, event.messageID ); } if (Reply.searchMsgID) { try { api.unsendMessage(Reply.searchMsgID); } catch (err) { console.error("Unsend search message error:", err); } } const video = Reply.results[choice - 1]; const videoId = video?.videoId || video?.id; if (!videoId) { api.setMessageReaction("โŒ", event.messageID, () => {}, true); return api.sendMessage("โŒ **Video ID not found!**", event.threadID); } const youtubeUrl = `https://youtube.com/watch?v=${videoId}`; api.setMessageReaction("โณ", event.messageID, () => {}, true); let filePath = null; const cleanup = () => { if (filePath && fs.existsSync(filePath)) fs.unlink(filePath, () => {}); }; // Reject JSON/HTML error bodies saved as media const looksLikeError = (file) => { const size = fs.statSync(file).size; if (size > 50 * 1024) return false; const head = fs.readFileSync(file, { encoding: "utf8" }).slice(0, 200).trim(); return /^[{<]/.test(head) || /error|expired|forbidden/i.test(head); }; const downloadFile = async (url, ext, safeId) => { const r = await axios.get(url, { responseType: "stream", timeout: 180000, maxRedirects: 5, headers: DL_HEADERS, validateStatus: (s) => s >= 200 && s < 300 }); const ctype = String(r.headers["content-type"] || "").toLowerCase(); if (/text\/html|application\/json/.test(ctype)) { r.data.destroy(); throw new Error(`Bad content-type: ${ctype}`); } const len = Number(r.headers["content-length"] || 0); if (len > MAX_SIZE) { r.data.destroy(); const e = new Error("File too large"); e.code = "TOO_BIG"; throw e; } filePath = path.join(cacheDir, `${safeId}_${Date.now()}.${ext}`); await pipeline(r.data, fs.createWriteStream(filePath)); const size = fs.statSync(filePath).size; if (size <= 0) throw new Error("File is empty"); if (size > MAX_SIZE) { const e = new Error("File too large"); e.code = "TOO_BIG"; throw e; } if (looksLikeError(filePath)) throw new Error("Server returned an error body"); return size; }; try { const MP3_API = await getBaseApi(); // download API base (with fallback) const safeId = String(videoId).replace(/[^a-zA-Z0-9_-]/g, "_"); // The API stores the file on its own server and returns a ready link // (valid ~5 minutes), so we download it right away. let info = await fetchMp3Link(MP3_API, youtubeUrl); const title = info.title || video?.title || "Unknown Song"; const channel = info.author || video?.author || ""; // Skip the download if the API already reports a file that is too big if (Number(info.filesize) > MAX_SIZE) { const e = new Error("File too large"); e.code = "TOO_BIG"; throw e; } let size = 0; let lastErr = null; for (let attempt = 1; attempt <= 3; attempt++) { try { size = await downloadFile(info.download_url, "mp3", safeId); lastErr = null; break; } catch (e) { lastErr = e; console.error( `[SING] download attempt ${attempt} failed:`, e?.response?.status || e?.code || "", e?.message, errBody(e) ); cleanup(); filePath = null; if (e.code === "TOO_BIG") break; // After 2 failures, request a fresh link in case it expired if (attempt === 2) { try { info = await fetchMp3Link(MP3_API, youtubeUrl); } catch (_) {} } if (attempt < 3) await sleep(2000); } } if (lastErr || !filePath) throw lastErr || new Error("No file downloaded"); api.sendMessage( { body: "๐ŸŽถ **NOW PLAYING** ๐ŸŽถ\n" + "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n" + `๐ŸŽต **Title:** ${title}\n` + (channel ? `๐Ÿ“บ **Channel:** ${channel}\n` : "") + `๐Ÿ“ฆ **Size:** ${(size / 1024 / 1024).toFixed(2)} MB\n` + "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n" + "๐Ÿ’ฟ **MAHABUB-BOT** โœจ", attachment: fs.createReadStream(filePath) }, event.threadID, (err) => { if (err) console.error("[SING] Send audio error:", err); cleanup(); }, event.messageID ); api.setMessageReaction("๐ŸŽต", event.messageID, () => {}, true); } catch (err) { console.error( "[SING] DOWNLOAD ERROR:", err?.response?.status || err?.code || "", err?.message, errBody(err) ); cleanup(); api.setMessageReaction("โŒ", event.messageID, () => {}, true); api.sendMessage( err?.code === "TOO_BIG" ? "โŒ **File too large to send!** (max 25 MB)" : "โŒ **Audio download error!**\nPlease try again later.", event.threadID ); } } };