const axios = require("axios"); const fs = require('fs-extra'); const path = require('path'); const baseApiUrl = async () => { try { const base = await axios.get(`https://raw.githubusercontent.com/mahmudx7/HINATA/main/baseApiUrl.json`); return base.data.mahmud; } catch (e) { throw new Error("API base URL fetching failed!"); } }; module.exports = { config: { name: "song", version: "2.0.0", author: "RS RIFAT", countDown: 5, role: 0, description: { bn: "ইউটিউব থেকে গান ডাউনলোড করুন", en: "Download songs/audio from YouTube", vi: "Tải nhạc từ YouTube" }, category: "music", guide: { bn: ' {pn} [গানের নাম বা লিঙ্ক]\n উদাহরণ: {pn} stay justin bieber', en: ' {pn} [song name or link]\n Example: {pn} stay justin bieber', vi: ' {pn} [tên bài hát hoặc link]\n Ví dụ: {pn} see you again' } }, langs: { bn: { error: "❌ | সমস্যা হয়েছে: %1", noResult: "⭕ | দুঃখিত, \"%1\" এর জন্য কোনো গান পাওয়া যায়নি।", success: "✅ | ডাউনলোড সম্পন্ন: %1" }, en: { error: "❌ | An error occurred: %1", noResult: "⭕ | No search results match: %1", success: "✅ | Successfully Downloaded: %1" } }, onStart: async function ({ api, args, event, commandName, getLang }) { const authorName = String.fromCharCode(82, 83, 32, 82, 73, 70, 65, 84); // RS RIFAT if (this.config.author !== authorName) { return api.sendMessage("You are not authorized to change the author name.", event.threadID, event.messageID); } const { threadID, messageID, senderID } = event; const input = args.join(" ").trim(); if (!input) return api.sendMessage("• Please provide a song name or YouTube link.", threadID, messageID); try { const apiUrl = await baseApiUrl(); const checkurl = /^(?:https?:\/\/)?(?:m\.|www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/))((\w|-){11})(?:\S+)?$/; // Direct Link Download if (checkurl.test(input)) { const videoID = input.match(checkurl)[1]; api.setMessageReaction("⌛", messageID, () => {}, true); return handleDownload(api, threadID, messageID, videoID, apiUrl, getLang); } // Search Songs api.setMessageReaction("⏳", messageID, () => {}, true); const res = await axios.get(`${apiUrl}/api/ytb/search?q=${encodeURIComponent(input)}`); const results = res.data && res.data.results ? res.data.results.slice(0, 6) : []; if (results.length === 0) { api.setMessageReaction("❌", messageID, () => {}, true); return api.sendMessage(getLang("noResult", input), threadID, messageID); } let msg = "🎶 ──「 CHOOSE YOUR SONG 」── 🎶\n\n"; const cacheDir = path.join(__dirname, 'cache'); if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true }); const thumbPaths = []; // Download all 6 thumbnails fast in parallel const downloadPromises = results.map(async (item, i) => { msg += `📌 ${i + 1}. ${item.title}\n⏳ Duration: ${item.time || 'N/A'}\n\n`; if (item.thumbnail) { const thumbPath = path.join(cacheDir, `thumb_${senderID}_${Date.now()}_${i}.jpg`); try { const thumbRes = await axios.get(item.thumbnail, { responseType: 'arraybuffer' }); fs.writeFileSync(thumbPath, Buffer.from(thumbRes.data)); return thumbPath; } catch (err) { return null; } } return null; }); const downloadedThumbs = await Promise.all(downloadPromises); // Filter valid image paths and create read streams const validPaths = downloadedThumbs.filter(p => p !== null && fs.existsSync(p)); const attachments = validPaths.map(p => fs.createReadStream(p)); msg += "💬 Reply with the song number (1-6) to download!"; return api.sendMessage({ body: msg, attachment: attachments }, threadID, (err, info) => { // Delete image cache safely after message is sent validPaths.forEach(p => { if (fs.existsSync(p)) fs.unlinkSync(p); }); if (err) return api.sendMessage(getLang("error", err.message), threadID, messageID); api.setMessageReaction("✅", messageID, () => {}, true); global.GoatBot.onReply.set(info.messageID, { commandName, author: senderID, results, apiUrl, messageID: info.messageID }); }, messageID); } catch (e) { api.setMessageReaction("❌", messageID, () => {}, true); return api.sendMessage(getLang("error", e.message || "Something went wrong!"), threadID, messageID); } }, onReply: async function ({ event, api, Reply, getLang }) { const { results, apiUrl, author, messageID: replyMsgID } = Reply; if (event.senderID !== author) return; const choice = parseInt(event.body.trim()); if (isNaN(choice) || choice <= 0 || choice > results.length) { return api.sendMessage("⚠️️ Invalid selection! Please reply with a valid number.", event.threadID, event.messageID); } const selectedVideo = results[choice - 1]; const videoID = selectedVideo.id; if (replyMsgID) api.unsendMessage(replyMsgID); api.setMessageReaction("📥", event.messageID, () => {}, true); await handleDownload(api, event.threadID, event.messageID, videoID, apiUrl, getLang); } }; async function handleDownload(api, threadID, messageID, videoID, apiUrl, getLang) { const cacheDir = path.join(__dirname, 'cache'); if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true }); const filePath = path.join(cacheDir, `song_${Date.now()}.mp3`); try { const res = await axios.get(`${apiUrl}/api/ytb/get?id=${videoID}&type=audio`); if (!res.data || !res.data.data || !res.data.data.downloadLink) { throw new Error("Download link not found from API."); } const { title, downloadLink } = res.data.data; const response = await axios({ url: downloadLink, method: 'GET', responseType: 'stream' }); const writer = fs.createWriteStream(filePath); response.data.pipe(writer); writer.on('finish', () => { api.sendMessage({ body: getLang("success", title || "Music Track"), attachment: fs.createReadStream(filePath) }, threadID, (err) => { api.setMessageReaction("✅", messageID, () => {}, true); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); }, messageID); }); writer.on('error', (err) => { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); api.setMessageReaction("❌", messageID, () => {}, true); api.sendMessage(getLang("error", "Stream writing failed!"), threadID, messageID); }); } catch (e) { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); api.setMessageReaction("❌", messageID, () => {}, true); api.sendMessage(getLang("error", e.message || "Download failed!"), threadID, messageID); } }