goat
command
sing
No description
1
views
0
likes
1
installs
Aliases: music, play
raw source
const axios = require("axios");
const fs = require('fs-extra');
const path = require('path');
const yts = require('yt-search');
const { Transform } = require("stream");
const { pipeline } = require("stream/promises");
const REACT = {
loading: "🐤",
success: "🪶",
error: "❌",
};
const AUDIO_API = "https://yt-song-api.vercel.app/api/song";
const VIDEO_API = "https://video-dl-api-tan.vercel.app";
const MAX_FILE_SIZE = 25 * 1024 * 1024;
const VIDEO_QUALITY = "720";
function isTrustedAudioHost(hostname) {
return hostname === "vidssave.com" || hostname.endsWith(".vidssave.com") ||
hostname === "ymcdn.org" || hostname.endsWith(".ymcdn.org") ||
hostname === "googlevideo.com" || hostname.endsWith(".googlevideo.com");
}
function getAudioUrl(data) {
const value = data && (data.download || data.audio_url);
if (!value) return null;
try {
const url = new URL(value);
return url.protocol === "https:" && isTrustedAudioHost(url.hostname) ? url.toString() : null;
} catch {
return null;
}
}
function fileSizeGuard(maxBytes) {
let received = 0;
return new Transform({
transform(chunk, _encoding, callback) {
received += chunk.length;
if (received > maxBytes) {
const error = new Error("File is too large to send on Messenger");
error.code = "SONG_TOO_LARGE";
return callback(error);
}
callback(null, chunk);
}
});
}
async function removeFile(filePath) {
if (!filePath) return;
await fs.promises.unlink(filePath).catch(() => {});
}
function sendMessageAsync(api, msgObj, threadID, messageID) {
return new Promise((resolve, reject) => {
api.sendMessage(msgObj, threadID, (err, info) => {
if (err) return reject(err instanceof Error ? err : new Error(String(err)));
resolve(info);
}, messageID);
});
}
module.exports = {
config: {
name: "sing",
aliases: ["music", "play"],
version: "2.1.1",
author: "Arafat",
countDown: 10,
role: 0,
description: {
vi: "Tự động tìm và gửi bài hát gốc từ YouTube",
en: "Automatically find and send the original song from YouTube"
},
category: "media",
guide: {
vi: " {pn} <tên bài hát>: gửi audio bài hát gốc"
+ "\n {pn} -v <tên bài hát>: gửi video bài hát gốc"
+ "\n Ví dụ:"
+ "\n {pn} pal pal"
+ "\n {pn} -v pal pal",
en: " {pn} <song name> → Audio"
+ "\n {pn} -v <song name> → Video"
+ "\n Example:"
+ "\n {pn} pal pal"
+ "\n {pn} -v pal pal"
}
},
langs: {
vi: {
error: "❌ | Không thể tải xuống. Vui lòng thử lại.",
noResult: "⭕ Không có kết quả tìm kiếm nào phù hợp với từ khóa %1"
},
en: {
error: "❌ | Could not download. Please try again.",
noResult: "⭕ No search results match the keyword %1"
}
},
onStart: async function ({ api, args, event, message, getLang }) {
const { threadID, messageID, senderID } = event;
let type = "audio";
if (args[0] === "-v" || args[0] === "video") {
type = "video";
args.shift();
} else if (args[0] === "-a" || args[0] === "audio") {
args.shift();
}
const input = args.join(" ");
if (!input) {
return message.reply("📌 Usage:\nsing <song name> → Audio\nsing -v <title> → Video");
}
if (type === "audio") {
return handleAudioDownload(api, threadID, messageID, senderID, input, getLang);
}
const checkurl = /^(?:https?:\/\/)?(?:m\.|www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/))((\w|-){11})(?:\S+)?$/;
api.setMessageReaction(REACT.loading, messageID, () => {}, true);
if (checkurl.test(input)) {
const videoID = input.match(checkurl)[1];
return handleDownload(api, threadID, messageID, videoID, type, getLang);
}
try {
const { videos } = await yts(`${input} official`);
if (!videos || videos.length === 0) {
api.setMessageReaction(REACT.error, messageID, () => {}, true);
return api.sendMessage(getLang("noResult", input), threadID, messageID);
}
const results = videos.map(v => ({ id: v.videoId, title: v.title }));
const videoID = pickBestResult(results, input).id;
return handleDownload(api, threadID, messageID, videoID, type, getLang);
} catch (e) {
api.setMessageReaction(REACT.error, messageID, () => {}, true);
return api.sendMessage(getLang("error"), threadID, messageID);
}
}
};
function pickBestResult(results, query) {
const q = query.toLowerCase();
const officialClean = results.find(r => {
const title = (r.title || "").toLowerCase();
return title.includes(q) && title.includes("official")
&& !title.includes("audio") && !title.includes("lyric");
});
if (officialClean) return officialClean;
const cleanMatch = results.find(r => {
const title = (r.title || "").toLowerCase();
return title.includes(q) && !title.includes("audio") && !title.includes("lyric");
});
if (cleanMatch) return cleanMatch;
const exactMatch = results.find(r => (r.title || "").toLowerCase().includes(q));
if (exactMatch) return exactMatch;
return results[0];
}
async function handleAudioDownload(api, threadID, messageID, senderID, songName, getLang) {
api.setMessageReaction(REACT.loading, messageID, () => {}, true);
const CACHE_DIR = path.join(__dirname, 'cache');
let filePath;
try {
const { data } = await axios.get(
AUDIO_API,
{
params: { q: `${songName} official` },
timeout: 45000,
headers: { Accept: "application/json" }
}
);
const audioUrl = getAudioUrl(data);
if (!data || !data.success || !audioUrl) {
api.setMessageReaction(REACT.error, messageID, () => {}, true);
return api.sendMessage(getLang("noResult", songName), threadID, messageID);
}
await fs.promises.mkdir(CACHE_DIR, { recursive: true });
const extension = ["mp3", "m4a", "mp4"].includes(data.format) ? data.format : "mp3";
filePath = path.join(
CACHE_DIR,
`sing_${senderID || "user"}_${Date.now()}.${extension}`
);
const response = await axios.get(audioUrl, {
responseType: "stream",
timeout: 90000,
maxRedirects: 3,
beforeRedirect: (options) => {
if (options.protocol !== "https:" || !isTrustedAudioHost(options.hostname)) {
throw new Error("Blocked an untrusted audio redirect");
}
},
headers: {
Accept: "audio/mpeg,audio/mp4,audio/*;q=0.9,*/*;q=0.1",
"User-Agent": "Mozilla/5.0"
}
});
const declaredSize = Number(response.headers["content-length"] || 0);
if (declaredSize > MAX_FILE_SIZE) {
response.data.destroy();
const error = new Error("Song is too large to send on Messenger");
error.code = "SONG_TOO_LARGE";
throw error;
}
const contentType = String(response.headers["content-type"] || "").toLowerCase();
if (contentType && !contentType.startsWith("audio/") && contentType !== "application/octet-stream") {
response.data.destroy();
throw new Error("Song provider returned an invalid audio file");
}
await pipeline(
response.data,
fileSizeGuard(MAX_FILE_SIZE),
fs.createWriteStream(filePath, { flags: "wx" })
);
const body = `✅ | 𝐇𝐞𝐫𝐞'𝐬 𝐲𝐨𝐮𝐫 𝐫𝐞𝐪𝐮𝐞𝐬𝐭𝐞𝐝 𝐬𝐨𝐧𝐠\n➡️ ${data.title || "Unknown"}`;
await sendMessageAsync(api, {
body,
attachment: fs.createReadStream(filePath)
}, threadID, messageID);
api.setMessageReaction(REACT.success, messageID, () => {}, true);
} catch (err) {
api.setMessageReaction(REACT.error, messageID, () => {}, true);
if (err.code === "SONG_TOO_LARGE") {
return api.sendMessage("❌ | This song is over 25 MB, so Messenger cannot send it.", threadID, messageID);
}
if (err.code === "ECONNABORTED") {
return api.sendMessage("❌ | Song request timed out. Please try again.", threadID, messageID);
}
return api.sendMessage(getLang("error"), threadID, messageID);
} finally {
await removeFile(filePath);
}
}
async function handleDownload(api, threadID, messageID, videoID, type, getLang) {
const format = 'mp4';
const cacheDir = path.join(__dirname, 'cache');
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
const filePath = path.join(cacheDir, `sing_${Date.now()}.${format}`);
const youtubeUrl = `https://www.youtube.com/watch?v=${videoID}`;
let title = "Unknown";
try {
const { data } = await axios.get(`${VIDEO_API}/download`, {
params: { url: youtubeUrl },
timeout: 45000
});
title = data.title || data.filename || data.name || "Unknown";
} catch (_) {}
try {
const streamUrl = `${VIDEO_API}/stream?url=${encodeURIComponent(youtubeUrl)}&type=video&quality=${VIDEO_QUALITY}`;
const response = await axios.get(streamUrl, {
responseType: "stream",
timeout: 90000,
headers: { Accept: "video/mp4,video/*;q=0.9,*/*;q=0.1" }
});
const declaredSize = Number(response.headers["content-length"] || 0);
if (declaredSize > MAX_FILE_SIZE) {
response.data.destroy();
const error = new Error("Video is too large to send on Messenger");
error.code = "SONG_TOO_LARGE";
throw error;
}
const contentType = String(response.headers["content-type"] || "").toLowerCase();
if (contentType && !contentType.startsWith("video/") && contentType !== "application/octet-stream") {
response.data.destroy();
throw new Error("Video provider returned an invalid file");
}
await pipeline(
response.data,
fileSizeGuard(MAX_FILE_SIZE),
fs.createWriteStream(filePath, { flags: "w" })
);
const body = `• ✨𝐓𝐢𝐭𝐥𝐞: ${title}`;
await sendMessageAsync(api, {
body,
attachment: fs.createReadStream(filePath)
}, threadID, messageID);
api.setMessageReaction(REACT.success, messageID, () => {}, true);
} catch (err) {
api.setMessageReaction(REACT.error, messageID, () => {}, true);
if (err.code === "SONG_TOO_LARGE") {
return api.sendMessage("❌ | This video is over 25 MB, so Messenger cannot send it.", threadID, messageID);
}
return api.sendMessage(getLang("error"), threadID, messageID);
} finally {
await removeFile(filePath);
}
}