← back to browse
goat command

yt

No description

0
views
0
likes
0
installs
raw source
const axios = require("axios");
const fs = require("fs-extra");
const os = require("os");
const path = require("path");
const { createCanvas, loadImage } = require("canvas");
 
const TMP_DIR = path.join(__dirname, "cache", "ytb");
const PER_PAGE = 8;
 
const REACT = {
    loading: "⏳",
    success: "✅",
    error: "❌",
};
 
const baseApiUrl = async () => {
    const base = await axios.get(`https://raw.githubusercontent.com/mahmudx7/HINATA/main/baseApiUrl.json`);
    return base.data.mahmud;
};
 
function truncate(text, maxLen) {
    if (!text) return "Untitled";
    return text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text;
}
 
function formatCount(n) {
    n = Number(n);
    if (!n || isNaN(n)) return null;
    if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
    if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
    if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
    return String(n);
}
 
function getChannel(v) {
    return v.channel?.name || v.channel || v.author || "Unknown";
}
 
function getDuration(v) {
    return v.time || v.duration_raw || v.duration || "";
}
 
function getViews(v) {
    return formatCount(v.views || v.view_count || v.viewCount);
}
 
const W = 640, HEADER_H = 80, ROW_H = 90, PADDING = 20, THUMB_W = 118, THUMB_H = 66;
 
function drawYouTubeLogo(ctx, x, y) {
    const rw = 40, rh = 28, r = 8;
    ctx.fillStyle = "#FF0000";
    ctx.beginPath();
    ctx.moveTo(x + r, y); ctx.lineTo(x + rw - r, y);
    ctx.quadraticCurveTo(x + rw, y, x + rw, y + r);
    ctx.lineTo(x + rw, y + rh - r);
    ctx.quadraticCurveTo(x + rw, y + rh, x + rw - r, y + rh);
    ctx.lineTo(x + r, y + rh);
    ctx.quadraticCurveTo(x, y + rh, x, y + rh - r);
    ctx.lineTo(x, y + r);
    ctx.quadraticCurveTo(x, y, x + r, y);
    ctx.closePath(); ctx.fill();
 
    const cx = x + rw / 2, cy = y + rh / 2;
    ctx.fillStyle = "#ffffff";
    ctx.beginPath();
    ctx.moveTo(cx - 5, cy - 8);
    ctx.lineTo(cx - 5, cy + 8);
    ctx.lineTo(cx + 9, cy);
    ctx.closePath(); ctx.fill();
}
 
async function generateSearchImage(pageResults, query, page, maxPage) {
    const totalH = HEADER_H + pageResults.length * ROW_H + 20;
    const canvas = createCanvas(W, totalH);
    const ctx = canvas.getContext("2d");
 
    ctx.fillStyle = "#0f0f0f";
    ctx.fillRect(0, 0, W, totalH);
    drawYouTubeLogo(ctx, PADDING, 22);
 
    ctx.fillStyle = "#ffffff"; ctx.font = "bold 22px sans-serif";
    ctx.fillText("YouTube Results", PADDING + 50, 43);
    ctx.fillStyle = "#aaaaaa"; ctx.font = "13px sans-serif";
    ctx.fillText(`"${truncate(query, 40)}" — Page ${page}/${maxPage}`, PADDING + 50, 62);
 
    ctx.strokeStyle = "#333333"; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(PADDING, HEADER_H - 1); ctx.lineTo(W - PADDING, HEADER_H - 1); ctx.stroke();
 
    for (let i = 0; i < pageResults.length; i++) {
        const v = pageResults[i];
        const y = HEADER_H + i * ROW_H;
        const mid = y + ROW_H / 2;
 
        if (i % 2 === 0) { ctx.fillStyle = "#181818"; ctx.fillRect(0, y, W, ROW_H); }
 
        ctx.fillStyle = "#666666"; ctx.font = "bold 18px sans-serif";
        ctx.fillText(String(i + 1), PADDING, mid + 7);
 
        const thumbX = PADDING + 30, thumbY = y + (ROW_H - THUMB_H) / 2;
        ctx.fillStyle = "#333333"; ctx.fillRect(thumbX, thumbY, THUMB_W, THUMB_H);
 
        try {
            const thumb = v.thumbnail;
            const imgBuf = await axios.get(thumb, {
                responseType: "arraybuffer",
                timeout: 6000,
                headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" }
            });
            ctx.drawImage(await loadImage(Buffer.from(imgBuf.data)), thumbX, thumbY, THUMB_W, THUMB_H);
        } catch {
            ctx.fillStyle = "#888888"; ctx.font = "11px sans-serif";
            ctx.fillText("No image", thumbX + 28, thumbY + 36);
        }
 
        ctx.strokeStyle = "#444444"; ctx.lineWidth = 1;
        ctx.strokeRect(thumbX, thumbY, THUMB_W, THUMB_H);
 
        const dur = getDuration(v);
        if (dur) {
            ctx.fillStyle = "rgba(0,0,0,0.75)";
            ctx.fillRect(thumbX, thumbY + THUMB_H - 16, THUMB_W, 16);
            ctx.fillStyle = "#ffffff"; ctx.font = "10px sans-serif";
            ctx.fillText(dur, thumbX + 4, thumbY + THUMB_H - 5);
        }
 
        const textX = thumbX + THUMB_W + 14;
        ctx.fillStyle = "#ffffff"; ctx.font = "bold 14px sans-serif";
        ctx.fillText(truncate(v.title, 52), textX, mid - 14);
        ctx.fillStyle = "#FF0000"; ctx.font = "12px sans-serif";
        ctx.fillText(truncate(getChannel(v), 32), textX, mid + 4);
 
        const views = getViews(v);
        ctx.fillStyle = "#777777"; ctx.font = "12px sans-serif";
        ctx.fillText(views ? `🎀 ${views} views` : dur ? `⏱ ${dur}` : "", textX, mid + 20);
 
        if (i < pageResults.length - 1) {
            ctx.strokeStyle = "#2a2a2a"; ctx.lineWidth = 1;
            ctx.beginPath(); ctx.moveTo(PADDING + 30, y + ROW_H); ctx.lineTo(W - PADDING, y + ROW_H); ctx.stroke();
        }
    }
 
    return canvas.toBuffer("image/jpeg", { quality: 0.92 });
}
 
async function sendPage(api, event, allResults, page, query, type, apiUrl, commandName, getLang) {
    await fs.ensureDir(TMP_DIR);
 
    const start = (page - 1) * PER_PAGE;
    const end = start + PER_PAGE;
    const pageResults = allResults.slice(start, end);
    const maxPage = Math.ceil(allResults.length / PER_PAGE);
 
    const imgBuf = await generateSearchImage(pageResults, query, page, maxPage);
    const tmpImg = path.join(os.tmpdir(), `yt_search_${Date.now()}.jpg`);
    fs.writeFileSync(tmpImg, imgBuf);
 
    return new Promise((resolve) => {
        api.sendMessage(
            {
                body: getLang("choose", `𝐘𝐨𝐮𝐓𝐮𝐛𝐞 𝐫𝐞𝐬𝐮𝐥𝐭𝐬 𝐟𝐨𝐫 "${query}" — page ${page}/${maxPage}\n`),
                attachment: fs.createReadStream(tmpImg)
            },
            event.threadID,
            (err, info) => {
                setTimeout(() => { try { fs.unlinkSync(tmpImg); } catch (_) {} }, 20000);
 
                if (err) {
                    console.error("Send page error:", err);
                    return resolve();
                }
 
                global.GoatBot.onReply.set(info.messageID, {
                    commandName,
                    author: event.senderID,
                    results: allResults,
                    query,
                    page,
                    type,
                    apiUrl,
                    menuMessageID: info.messageID
                });
 
                resolve();
            },
            event.messageID
        );
    });
}
 
module.exports = {
    config: {
        name: "yt",
        aliases: [],
        version: "3.0",
        author: "Arafat",
        countDown: 10,
        role: 0,
        description: {
            vi: "Tải video, audio hoặc xem thông tin video trên YouTube",
            en: "Download video, audio or view video information on YouTube"
        },
        category: "media",
        guide: {
            vi: "   {pn} [video|-v] [<tên video>|<link video>]: dùng để tải video từ youtube."
                + "\n   {pn} [audio|-a] [<tên video>|<link video>]: dùng để tải audio từ youtube"
                + "\n   {pn} [info|-i] [<tên video>|<link video>]: dùng để xem thông tin video từ youtube"
                + "\n   Ví dụ:"
                + "\n    {pn} -v Mood Lo-Fi"
                + "\n    {pn} -a Mood Lo-Fi"
                + "\n    {pn} -i Mood Lo-Fi",
            en: "   {pn} [video|-v] [<video name>|<video link>]: use to download video from youtube."
                + "\n   {pn} [audio|-a] [<video name>|<video link>]: use to download audio from youtube"
                + "\n   {pn} [info|-i] [<video name>|<video link>]: use to view video information from youtube"
                + "\n   Example:"
                + "\n    {pn} -v Mood Lo-Fi"
                + "\n    {pn} -a Mood Lo-Fi"
                + "\n    {pn} -i Mood Lo-Fi"
        }
    },
 
    langs: {
        vi: {
            error: "❌ | Could not download. Please try again.",
            noResult: "⭕ | Không có kết quả tìm kiếm nào phù hợp với từ khóa %1",
            choose: "%1",
            video: "video",
            audio: "âm thanh",
            noVideo: "⭕ | Rất tiếc, không tìm thấy video nào hợp lệ",
            noAudio: "⭕ | Rất tiếc, không tìm thấy audio nào hợp lệ",
            noMore: "😿 | Không còn kết quả nào nữa!",
            invalid: "😺 | Reply với số hợp lệ hoặc 'next'.",
            info: "💠 | Tiêu đề: %1\n🏪 Channel: %2\n👨‍👩‍👧‍👦 Subscriber: %3\n⏱ Thời gian video: %4\n👀 Lượt xem: %5\n👍 Lượt thích: %6\n🆙 Ngày tải lên: %7\n🔠 ID: %8\n🔗 Link: %9"
        },
        en: {
            error: "❌ | Could not download. Please try again.",
            noResult: "⭕ | No search results match the keyword",
            choose: "%1",
            video: "video",
            audio: "audio",
            noVideo: "⭕ | Sorry, no video was found",
            noAudio: "⭕ | Sorry, no audio was found",
            noMore: "😿 | No more results!",
            invalid: "😺 | Reply with a valid number or 'next'.",
            info: "💠 | Title: %1\n🏪 Channel: %2\n👨‍👩‍👧‍👦 Subscriber: %3\n⏱ Video duration: %4\n👀 View count: %5\n👍 Like count: %6\n🆙 Upload date: %7\n🔠 ID: %8\n🔗 Link: %9"
        }
    },
 
    onStart: async function ({ api, args, message, event, commandName, getLang }) {
        const { threadID, messageID } = event;
 
        let type;
        switch (args[0]) {
            case "-v":
            case "video":
                type = "video";
                break;
            case "-a":
            case "-s":
            case "audio":
            case "sing":
                type = "audio";
                break;
            case "-i":
            case "info":
                type = "info";
                break;
            default:
                api.setMessageReaction(REACT.error, messageID, () => {}, true);
                return message.SyntaxError();
        }
 
        const input = args.slice(1).join(" ");
        if (!input) {
            api.setMessageReaction(REACT.error, messageID, () => {}, true);
            return message.SyntaxError();
        }
 
        const apiUrl = await baseApiUrl();
        const checkurl = /^(?:https?:\/\/)?(?:m\.|www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/))((\w|-){11})(?:\S+)?$/;
 
        if (checkurl.test(input)) {
            const videoID = input.match(checkurl)[1];
            api.setMessageReaction(REACT.loading, messageID, () => {}, true);
            if (type === 'info') return fetchInfo(api, threadID, messageID, videoID, apiUrl, getLang);
            return handleDownload(api, threadID, messageID, videoID, type, apiUrl, getLang);
        }
 
        try {
            api.setMessageReaction(REACT.loading, messageID, () => {}, true);
            const res = await axios.get(`${apiUrl}/api/ytb/search?q=${encodeURIComponent(input)}`);
            const allResults = (res.data.results || []).slice(0, 32);
            if (!allResults.length) {
                api.setMessageReaction(REACT.error, messageID, () => {}, true);
                return api.sendMessage(getLang("noResult", input), threadID, messageID);
            }
 
            await sendPage(api, event, allResults, 1, input, type, apiUrl, commandName, getLang);
            api.setMessageReaction(REACT.success, messageID, () => {}, true);
 
        } catch (e) {
            api.setMessageReaction(REACT.error, messageID, () => {}, true);
            return api.sendMessage(getLang("error", e.message), threadID, messageID);
        }
    },
 
    onReply: async function ({ event, api, Reply, getLang }) {
        const { results, type, apiUrl, author, menuMessageID, page, query, commandName } = Reply;
        if (event.senderID !== author) return;
 
        const targetMessageID = menuMessageID || Reply.messageID;
        const body = (event.body || "").trim().toLowerCase();
 
        api.setMessageReaction(REACT.loading, event.messageID, () => {}, true);
 
        if (body === "next") {
            const nextPage = page + 1;
            const maxPage = Math.ceil(results.length / PER_PAGE);
            if (nextPage > maxPage) {
                api.setMessageReaction(REACT.error, event.messageID, () => {}, true);
                return api.sendMessage(getLang("noMore"), event.threadID, event.messageID);
            }
            try { api.unsendMessage(targetMessageID); } catch {}
            global.GoatBot.onReply.delete(targetMessageID);
            await sendPage(api, event, results, nextPage, query, type, apiUrl, commandName, getLang);
            api.setMessageReaction(REACT.success, event.messageID, () => {}, true);
            return;
        }
 
        const choice = parseInt(event.body);
        if (isNaN(choice) || choice <= 0 || choice > results.length) {
            api.setMessageReaction(REACT.error, event.messageID, () => {}, true);
            try { api.unsendMessage(targetMessageID); } catch {}
            global.GoatBot.onReply.delete(targetMessageID);
            return api.sendMessage(getLang("invalid"), event.threadID, event.messageID);
        }
 
        const videoID = results[choice - 1].id;
 
        try { api.unsendMessage(targetMessageID); } catch {}
        global.GoatBot.onReply.delete(targetMessageID);
 
        if (type === 'info') return fetchInfo(api, event.threadID, event.messageID, videoID, apiUrl, getLang);
        await handleDownload(api, event.threadID, event.messageID, videoID, type, apiUrl, getLang);
    }
};
 
async function handleDownload(api, threadID, messageID, videoID, type, apiUrl, getLang) {
    const format = type === 'audio' ? 'mp3' : 'mp4';
    const cacheDir = path.join(__dirname, 'cache');
    if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
 
    const filePath = path.join(cacheDir, `yt_${Date.now()}.${format}`);
 
    try {
        const res = await axios.get(`${apiUrl}/api/ytb/get?id=${videoID}&type=${type}`);
        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', () => {
            const body = type === "audio"
                ? `✅ | 𝐇𝐞𝐫𝐞'𝐬 𝐲𝐨𝐮𝐫 𝐫𝐞𝐪𝐮𝐞𝐬𝐭𝐞𝐝 𝐬𝐨𝐧𝐠\n➡️ ${title}`
                : `• ✨𝐓𝐢𝐭𝐥𝐞: ${title}`;
            api.sendMessage({
                body,
                attachment: fs.createReadStream(filePath)
            }, threadID, () => {
                api.setMessageReaction(REACT.success, messageID, () => {}, true);
                if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
            }, messageID);
        });
 
        writer.on('error', (err) => {
            api.setMessageReaction(REACT.error, messageID, () => {}, true);
            api.sendMessage(getLang("error", "Download failed!"), threadID, messageID);
            if (fs.existsSync(filePath)) { try { fs.unlinkSync(filePath); } catch (_) {} }
        });
    } catch (e) {
        api.setMessageReaction(REACT.error, messageID, () => {}, true);
        api.sendMessage(getLang("error", "Download failed!"), threadID, messageID);
    }
}
 
async function fetchInfo(api, threadID, messageID, videoID, apiUrl, getLang) {
    try {
        const res = await axios.get(`${apiUrl}/api/ytb/details?id=${videoID}`);
        const d = res.data.details;
 
        const formatNum = (num) => String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
 
        const msg = getLang("info",
            d.title, d.channel, formatNum(d.subCount || 0), d.duration_raw || d.duration,
            formatNum(d.view_count || 0), formatNum(d.like_count || 0), d.upload_date || 'N/A', videoID, d.webpage_url
        );
 
        const cacheDir = path.join(__dirname, 'cache');
        if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
 
        const thumbPath = path.join(cacheDir, `info_${videoID}.jpg`);
        const thumbRes = await axios.get(d.thumbnail, { responseType: 'arraybuffer' });
        fs.writeFileSync(thumbPath, Buffer.from(thumbRes.data));
 
        api.sendMessage({ body: msg, attachment: fs.createReadStream(thumbPath) },
            threadID, () => {
                api.setMessageReaction(REACT.success, messageID, () => {}, true);
                if (fs.existsSync(thumbPath)) fs.unlinkSync(thumbPath);
            }, messageID);
    } catch (e) {
        api.setMessageReaction(REACT.error, messageID, () => {}, true);
        api.sendMessage(getLang("error", e.message), threadID, messageID);
    }
      }

View raw file