const axios = require("axios"); const fs = require("fs-extra"); const path = require("path"); const http = axios.create({ timeout: 60000, maxRedirects: 5, headers: { "User-Agent": "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120 Mobile Safari/537.36", "Accept": "*/*" } }); const baseApiUrl = async () => { const res = await http.get( "https://raw.githubusercontent.com/mahmudx7/HINATA/main/baseApiUrl.json" ); const url = res.data?.mahmud || res.data?.baseUrl || res.data?.url; if (!url) { throw new Error("Base API URL not found"); } return String(url).replace(/\/+$/, ""); }; const safeRemove = async file => { try { if (file && await fs.pathExists(file)) { await fs.remove(file); } } catch (_) {} }; const getApiError = error => { const status = error.response?.status; const data = error.response?.data; if (status) { if (typeof data === "string") { return `HTTP ${status}: ${data.slice(0, 300)}`; } return ( `HTTP ${status}: ` + ( data?.message || data?.error || data?.msg || "Server returned an error" ) ); } return error.message || "Unknown error"; }; module.exports = { config: { name: "song", version: "3.0", author: "ASIF", countDown: 5, role: 0, description: { bn: "গান সার্চ ও অডিও ডাউনলোড", en: "Search and download songs/audio", vi: "Tìm kiếm và tải nhạc" }, category: "music", guide: { bn: "{pn} [গানের নাম বা YouTube লিংক]\n" + "উদাহরণ: {pn} tui chinli na amay", en: "{pn} [song name or YouTube link]\n" + "Example: {pn} stay justin bieber", vi: "{pn} [tên bài hát hoặc link YouTube]" } }, langs: { bn: { error: "❌ | সমস্যা হয়েছে: %1", noResult: '⭕ | "%1" এর জন্য কোনো গান পাওয়া যায়নি।', choose: "🎵 গানগুলোর তালিকা:\n\n%1\n" + "━━━━━━━━━━━━━━\n" + "👉 Reply করে 1-6 এর মধ্যে একটি সংখ্যা দিন।", processing: "⏳ | গান প্রস্তুত করা হচ্ছে...", apiError: "❌ | API কোনো valid download URL দেয়নি।", serverError: "❌ | Song API Server Error: %1" }, en: { error: "❌ | An error occurred: %1", noResult: '⭕ | No songs found for "%1".', choose: "🎵 Song Results:\n\n%1\n" + "━━━━━━━━━━━━━━\n" + "👉 Reply with a number from 1-6.", processing: "⏳ | Preparing song...", apiError: "❌ | API did not return a valid download URL.", serverError: "❌ | Song API Server Error: %1" } }, onStart: async function ({ api, args, event, commandName, getLang }) { const { threadID, messageID, senderID } = event; const input = args.join(" ").trim(); if (!input) { return api.sendMessage( "🎵 Please enter a song name or YouTube link.", threadID, messageID ); } let apiUrl; try { apiUrl = await baseApiUrl(); } catch (error) { console.log( "[SONG BASE API ERROR]", error.message ); return api.sendMessage( getLang( "error", "Unable to connect to Base API." ), threadID, messageID ); } /* * YouTube URL detector */ const checkurl = /^(?:https?:\/\/)?(?:m\.|www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/))((\w|-){11})(?:\S+)?$/i; /* * Direct YouTube link */ if (checkurl.test(input)) { const match = input.match(checkurl); if (!match?.[1]) { return api.sendMessage( getLang( "error", "Invalid YouTube URL." ), threadID, messageID ); } api.setMessageReaction( "⌛", messageID, () => {}, true ); return handleDownload( api, threadID, messageID, match[1], apiUrl, getLang ); } /* * Search song */ try { api.setMessageReaction( "🔎", messageID, () => {}, true ); const searchUrl = `${apiUrl}/api/ytb/search?q=${encodeURIComponent(input)}`; console.log( "[SONG SEARCH]", searchUrl ); const res = await http.get(searchUrl); console.log( "[SONG SEARCH STATUS]", res.status ); const rawResults = res.data?.results || res.data?.data?.results || res.data?.result || []; if (!Array.isArray(rawResults)) { console.log( "[SONG SEARCH INVALID RESPONSE]", res.data ); throw new Error( "Invalid search API response" ); } const results = rawResults .filter(item => { return ( item && ( item.id || item.videoId || item.video_id ) ); }) .slice(0, 6); if (results.length === 0) { return api.sendMessage( getLang( "noResult", input ), threadID, messageID ); } let msg = ""; const attachments = []; const cacheDir = path.join( __dirname, "cache" ); await fs.ensureDir(cacheDir); for ( let i = 0; i < results.length; i++ ) { const item = results[i]; const title = item.title || item.name || "Unknown Song"; const time = item.time || item.duration || item.length || "Unknown"; const thumbnail = item.thumbnail || item.thumb || item.image || item.thumbnailUrl || null; msg += `${i + 1}. ${title}\n` + `⏱️ ${time}\n\n`; /* * Thumbnail is optional. * If it fails, song search continues. */ if (thumbnail) { try { const thumbPath = path.join( cacheDir, `thumb_${senderID}_${Date.now()}_${i}.jpg` ); const thumb = await http.get( thumbnail, { responseType: "arraybuffer", timeout: 15000 } ); await fs.writeFile( thumbPath, Buffer.from( thumb.data ) ); attachments.push( fs.createReadStream( thumbPath ) ); } catch (thumbError) { console.log( "[THUMBNAIL ERROR]", thumbError.message ); } } } return api.sendMessage( { body: getLang( "choose", msg ), ...(attachments.length ? { attachment: attachments } : {}) }, threadID, (err, info) => { /* * Remove thumbnails */ for ( const stream of attachments ) { try { if ( stream.path && fs.existsSync( stream.path ) ) { fs.unlinkSync( stream.path ); } } catch (_) {} } if ( !err && info && global.GoatBot?.onReply ) { global.GoatBot.onReply.set( info.messageID, { commandName, author: senderID, results, apiUrl, messageID: info.messageID } ); } }, messageID ); } catch (error) { console.log( "========== SONG SEARCH ERROR ==========" ); console.log( "Status:", error.response?.status ); console.log( "URL:", error.config?.url ); console.log( "Response:", error.response?.data ); console.log( "Message:", error.message ); console.log( "=======================================" ); const status = error.response?.status; if (status >= 500) { return api.sendMessage( getLang( "serverError", `HTTP ${status}` ), threadID, messageID ); } return api.sendMessage( getLang( "error", getApiError(error) ), threadID, messageID ); } }, onReply: async function ({ event, api, Reply, getLang }) { const { results, apiUrl, author, messageID } = Reply; if ( String(event.senderID) !== String(author) ) { return; } const choice = parseInt( String( event.body ).trim() ); if ( Number.isNaN(choice) || choice < 1 || choice > results.length ) { return api.sendMessage( `❌ Please choose a number between 1 and ${results.length}.`, event.threadID, event.messageID ); } const selected = results[choice - 1]; const videoID = selected.id || selected.videoId || selected.video_id; if (!videoID) { return api.sendMessage( getLang( "error", "Video ID not found." ), event.threadID, event.messageID ); } try { if (messageID) { api.unsendMessage( messageID ); } } catch (_) {} api.setMessageReaction( "⌛", event.messageID, () => {}, true ); return handleDownload( api, event.threadID, event.messageID, videoID, apiUrl, getLang ); } }; /* * ============================ * DOWNLOAD FUNCTION * ============================ */ async function handleDownload( api, threadID, messageID, videoID, apiUrl, getLang ) { const cacheDir = path.join( __dirname, "cache" ); await fs.ensureDir( cacheDir ); const filePath = path.join( cacheDir, `song_${Date.now()}_${Math.random() .toString(36) .slice(2)}.mp3` ); try { api.setMessageReaction( "⏳", messageID, () => {}, true ); /* * Request audio information */ const getUrl = `${apiUrl}/api/ytb/get?id=${encodeURIComponent(videoID)}&type=audio`; console.log( "[SONG DOWNLOAD API]", getUrl ); const res = await http.get( getUrl ); console.log( "[SONG DOWNLOAD STATUS]", res.status ); console.log( "[SONG DOWNLOAD RESPONSE]", JSON.stringify( res.data, null, 2 ).slice(0, 5000) ); /* * Find data object */ const data = res.data?.data || res.data?.result || res.data; /* * Find title */ const title = data?.title || data?.name || data?.songName || `Song_${videoID}`; /* * Find download URL */ const downloadLink = data?.downloadLink || data?.downloadUrl || data?.download_url || data?.audioUrl || data?.audio_url || data?.url || res.data?.downloadLink || res.data?.downloadUrl || res.data?.url; /* * No URL */ if ( !downloadLink || typeof downloadLink !== "string" ) { console.log( "[SONG ERROR] API response has no download URL." ); throw new Error( "Unable to generate a download URL" ); } /* * Validate URL */ if ( !/^https?:\/\//i.test( downloadLink ) ) { throw new Error( "API returned an invalid download URL" ); } console.log( "[SONG DOWNLOAD URL]", downloadLink ); /* * Download audio */ const response = await http.get( downloadLink, { responseType: "stream", timeout: 180000, validateStatus: status => status >= 200 && status < 400 } ); const writer = fs.createWriteStream( filePath ); response.data.pipe( writer ); await new Promise( (resolve, reject) => { writer.on( "finish", resolve ); writer.on( "error", reject ); response.data.on( "error", reject ); } ); /* * Check downloaded file */ if ( !(await fs.pathExists( filePath )) ) { throw new Error( "Audio file was not created" ); } const stat = await fs.stat( filePath ); if ( !stat.size || stat.size < 1000 ) { throw new Error( "Downloaded audio file is empty or invalid" ); } /* * Send song */ return api.sendMessage( { body: getLang( "success", title ), attachment: fs.createReadStream( filePath ) }, threadID, async () => { api.setMessageReaction( "✅", messageID, () => {}, true ); /* * Give Messenger time * to finish reading file */ setTimeout( async () => { await safeRemove( filePath ); }, 10000 ); }, messageID ); } catch (error) { console.log( "========== SONG DOWNLOAD ERROR ==========" ); console.log( "Status:", error.response?.status ); console.log( "URL:", error.config?.url ); console.log( "Response:", error.response?.data ); console.log( "Message:", error.message ); console.log( "==========================================" ); await safeRemove( filePath ); api.setMessageReaction( "❌", messageID, () => {}, true ); const status = error.response?.status; if (status >= 500) { return api.sendMessage( getLang( "serverError", `HTTP ${status}` ), threadID, messageID ); } return api.sendMessage( getLang( "error", error.message || "Download failed" ), threadID, messageID ); } }