// code from goatstore // api: https://store.agi.bd const axios = require("axios"); const fs = require("fs-extra"); const path = require("path"); const os = require("os"); const crypto = require("crypto"); const { Transform } = require("stream"); const { pipeline } = require("stream/promises"); const JAMENDO_TRACKS_API = "https://api.jamendo.com/v3.0/tracks/"; const MAX_AUDIO_SIZE = 25 * 1024 * 1024; module.exports = { config: { name: "song", aliases: ["sing", "play", "music"], version: "3.2.0", author: "ISHIGAMI⚑", countDown: 5, role: 0, description: { en: "Search Jamendo and send an artist-approved downloadable track" }, category: "media", guide: { en: "{pn} \n{pn} \n\nExample:\n{pn} ambient guitar" } }, onStart: async function ({ args, message }) { const query = args.join(" ").trim(); if (!query) { return message.reply( "🎡 Enter a song name or artist to search.\nExample: song ambient guitar" ); } const clientId = process.env.JAMENDO_CLIENT_ID; if (!clientId) { return message.reply( "⚠️ The bot admin needs to set JAMENDO_CLIENT_ID in the bot host environment." ); } let loadingMessage; let audioPath; try { loadingMessage = await message.reply( "πŸ” Searching Jamendo for downloadable music..." ); const trackId = parseJamendoTrackId(query); if (looksLikeLink(query) && !trackId) { throw new Error( "Use a Jamendo track link or search by song name; YouTube/Spotify links aren't audio sources here." ); } const track = await searchJamendo(query, clientId, trackId); await removeLoadingMessage(message, loadingMessage); loadingMessage = null; if (!track) { return message.reply( "❌ No matching track with downloads enabled by its artist was found." ); } loadingMessage = await message.reply( `⬇️ Downloading ${cleanText(track.name)} by ${cleanText(track.artist_name)}...` ); audioPath = await downloadTrack(track); await removeLoadingMessage(message, loadingMessage); loadingMessage = null; const attribution = [ `🎡 ${cleanText(track.name)}`, `πŸ‘€ ${cleanText(track.artist_name)}`, track.shareurl ? `πŸ”— ${track.shareurl}` : "", track.license_ccurl ? `πŸ“œ License: ${track.license_ccurl}` : "" ] .filter(Boolean) .join("\n"); await message.reply({ body: attribution, attachment: fs.createReadStream(audioPath) }); } catch (error) { await removeLoadingMessage(message, loadingMessage); console.error( "[song] Jamendo command error:", error.response?.data || error.message ); return message.reply(`❌ ${getUserFacingError(error)}`); } finally { if (audioPath) { try { await fs.remove(audioPath); } catch (error) { console.error( "[song] Could not remove temporary audio:", error.message ); } } } } }; async function searchJamendo(query, clientId, trackId) { const response = await axios.get(JAMENDO_TRACKS_API, { params: { client_id: clientId, format: "json", limit: 10, type: "single albumtrack", ...(trackId ? { id: trackId } : { search: query }), audioformat: "mp32", audiodlformat: "mp32", include: "licenses" }, timeout: 15000 }); if (response.data?.headers?.status !== "success") { throw new Error("Jamendo returned an unsuccessful API response."); } return ( (response.data.results || []).find( (item) => item.audiodownload_allowed === true && item.audiodownload ) || null ); } async function downloadTrack(track) { const tempDir = path.join(os.tmpdir(), "goatbot-song"); await fs.ensureDir(tempDir); const safeId = String(track.id).replace(/[^0-9]/g, "") || "track"; const filePath = path.join( tempDir, `jamendo-${safeId}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}.mp3` ); try { const response = await axios.get(track.audiodownload, { responseType: "stream", timeout: 30000, maxRedirects: 5 }); const contentType = String( response.headers?.["content-type"] || "" ).toLowerCase(); if ( contentType && !contentType.startsWith("audio/") && !contentType.includes("octet-stream") ) { response.data.destroy(); throw new Error("Jamendo returned a non-audio response."); } const contentLength = Number( response.headers?.["content-length"] || 0 ); if (contentLength > MAX_AUDIO_SIZE) { response.data.destroy(); throw new Error( "The track is larger than Messenger's 25 MB attachment limit." ); } let receivedBytes = 0; const sizeLimit = new Transform({ transform(chunk, encoding, callback) { receivedBytes += chunk.length; if (receivedBytes > MAX_AUDIO_SIZE) { callback( new Error( "The track is larger than Messenger's 25 MB attachment limit." ) ); return; } callback(null, chunk); } }); await pipeline( response.data, sizeLimit, fs.createWriteStream(filePath) ); if (receivedBytes === 0) { throw new Error("Jamendo returned an empty audio file."); } return filePath; } catch (error) { await fs.remove(filePath).catch(() => {}); throw error; } } function parseJamendoTrackId(input) { try { const normalized = /^https?:\/\//i.test(input) ? input : `https://${input}`; const url = new URL(normalized); const host = url.hostname.toLowerCase().replace(/^www\./, ""); if (!["jamendo.com", "jamen.do"].includes(host)) { return null; } const match = url.pathname.match(/\/track\/(\d+)/i) || url.pathname.match(/\/t\/(\d+)/i); return match ? match[1] : null; } catch { return null; } } function looksLikeLink(value) { return ( /^(?:https?:\/\/|www\.)/i.test(value) || /^(?:jamendo\.com|jamen\.do)\//i.test(value) ); } function cleanText(value) { return String(value || "Unknown").replace(/\s+/g, " ").trim(); } async function removeLoadingMessage(message, sentMessage) { if (!sentMessage?.messageID || typeof message.unsend !== "function") { return; } try { await message.unsend(sentMessage.messageID); } catch { // The command can still finish if the bot framework cannot remove the status message. } } function getUserFacingError(error) { if ( error.message === "Use a Jamendo track link or search by song name; YouTube/Spotify links aren't audio sources here." ) { return error.message; } if (error.message.includes("25 MB")) { return error.message; } if (error.response?.status === 401 || error.response?.status === 403) { return "Jamendo rejected the client ID. Check JAMENDO_CLIENT_ID in the bot host environment."; } if (error.code === "ECONNABORTED") { return "The music service took too long to respond. Please try again."; } return "Could not search or download this track. Please try another search."; }