← back to browse
goat command

xncdi

No description

0
views
0
likes
0
installs

Aliases: xvideo, xnxx, xv

raw source
"use strict";

const axios  = require("axios");
const fs     = require("fs-extra");
const path   = require("path");

let createCanvas, loadImage;
try { ({ createCanvas, loadImage } = require("canvas")); } catch {}

const BASE    = "https://xncdi.vercel.app";
const TMP_DIR = path.join(process.cwd(), "scripts", "tmp");
fs.ensureDirSync(TMP_DIR);

const truncate = (str = "", max = 50) => str.length > max ? str.slice(0, max - 1) + "…" : str;

const QUALITY_COLOR = (q = "") =>
    q.includes("1080") ? "#1e7aff" :
    q.includes("720")  ? "#22bb55" :
    q.includes("480")  ? "#e6a800" : "#888888";

const THUMB_GRADIENTS = [
    ["#3d0c0c", "#7a1a1a"],
    ["#0c1e3d", "#1a3a7a"],
    ["#0c3d1a", "#1a7a35"],
    ["#2a0c3d", "#501a7a"],
    ["#3d2a0c", "#7a551a"],
    ["#0c3d3d", "#1a7070"],
    ["#3d3d0c", "#7a7a1a"],
    ["#1a0c3d", "#351a7a"],
];

function react(botApi, event, emoji) {
    try { botApi.setMessageReaction(emoji, event.messageID, () => {}, true); } catch (_) {}
}

async function fetchRetry(url, params = {}, timeout = 20000) {
    for (let i = 0; i < 3; i++) {
        try {
            return await axios.get(url, { params, timeout });
        } catch (err) {
            if (i === 2) throw err;
            await new Promise(r => setTimeout(r, 1500));
        }
    }
}

function wrapText(ctx, text, x, y, maxWidth, lineHeight, maxLines) {
    const words = text.split(" ");
    let line = "", linesDrawn = 0;
    for (let i = 0; i < words.length; i++) {
        const test = line + (line ? " " : "") + words[i];
        if (ctx.measureText(test).width > maxWidth && line) {
            if (linesDrawn >= maxLines - 1) { ctx.fillText(line.trimEnd() + "…", x, y); return; }
            ctx.fillText(line, x, y);
            y += lineHeight;
            linesDrawn++;
            line = words[i];
        } else {
            line = test;
        }
    }
    if (line && linesDrawn < maxLines) ctx.fillText(line, x, y);
}

function roundRect(ctx, x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + w - r, y);
    ctx.quadraticCurveTo(x + w, y, x + w, y + r);
    ctx.lineTo(x + w, y + h - r);
    ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
    ctx.lineTo(x + r, y + h);
    ctx.quadraticCurveTo(x, y + h, x, y + h - r);
    ctx.lineTo(x, y + r);
    ctx.quadraticCurveTo(x, y, x + r, y);
    ctx.closePath();
}

async function makeXncdiCard(results, query, isTrending) {
    if (!createCanvas) return null;

    const COLS = 4, ROWS = Math.ceil(Math.min(results.length, 8) / COLS);
    const THUMB_W = 192, THUMB_H = 108, INFO_H = 52, GAP = 6;
    const PAD_X = 12, PAD_Y = 12, HEADER_H = 58, FOOTER_H = 36;
    const W = PAD_X * 2 + COLS * THUMB_W + (COLS - 1) * GAP;
    const H = HEADER_H + PAD_Y + ROWS * (THUMB_H + INFO_H + GAP) - GAP + PAD_Y + FOOTER_H;

    const canvas = createCanvas(W, H);
    const ctx    = canvas.getContext("2d");
    ctx.antialias = "subpixel";

    ctx.fillStyle = "#111111";
    ctx.fillRect(0, 0, W, H);

    const hGrad = ctx.createLinearGradient(0, 0, W, HEADER_H);
    hGrad.addColorStop(0, "#c0170f");
    hGrad.addColorStop(1, "#8c0f09");
    ctx.fillStyle = hGrad;
    ctx.fillRect(0, 0, W, HEADER_H);

    ctx.fillStyle = "#ffffff";
    ctx.font      = "bold 28px sans-serif";
    ctx.fillText("XNXX", PAD_X, 38);

    ctx.fillStyle = "rgba(255,255,255,0.3)";
    ctx.fillRect(74, 16, 2, 28);

    ctx.fillStyle = "rgba(255,255,255,0.85)";
    ctx.font      = "14px sans-serif";
    ctx.fillText(isTrending ? `🔥 Trending${query ? " — " + query : ""}` : `🔍 ${truncate(query, 38)}`, 84, 36);

    const countLabel = `${results.length} videos`;
    const countW     = ctx.measureText(countLabel).width + 16;
    ctx.fillStyle    = "rgba(0,0,0,0.4)";
    roundRect(ctx, W - PAD_X - countW, 17, countW, 22, 11);
    ctx.fill();
    ctx.fillStyle = "#fff";
    ctx.font      = "bold 11px sans-serif";
    ctx.fillText(countLabel, W - PAD_X - countW + 8, 32);

    const dotColors = ["#ff5f56", "#ffbd2e", "#27c93f"];
    for (let i = 0; i < 3; i++) {
        ctx.fillStyle = dotColors[i];
        ctx.beginPath();
        ctx.arc(PAD_X + i * 18, HEADER_H - 10, 5, 0, Math.PI * 2);
        ctx.fill();
    }

    for (let i = 0; i < Math.min(results.length, 8); i++) {
        const r   = results[i];
        const col = i % COLS;
        const row = Math.floor(i / COLS);
        const x   = PAD_X + col * (THUMB_W + GAP);
        const y   = HEADER_H + PAD_Y + row * (THUMB_H + INFO_H + GAP);

        const [gc1, gc2] = THUMB_GRADIENTS[i % THUMB_GRADIENTS.length];
        const tGrad      = ctx.createLinearGradient(x, y, x + THUMB_W, y + THUMB_H);
        tGrad.addColorStop(0, gc1);
        tGrad.addColorStop(1, gc2);
        ctx.fillStyle = tGrad;
        roundRect(ctx, x, y, THUMB_W, THUMB_H, 4);
        ctx.fill();

        const thumbUrl = r.thumbnail || r.thumb || r.image || r.cover || null;
        if (thumbUrl && loadImage) {
            try {
                const img = await loadImage(thumbUrl);
                ctx.save();
                roundRect(ctx, x, y, THUMB_W, THUMB_H, 4);
                ctx.clip();
                ctx.drawImage(img, x, y, THUMB_W, THUMB_H);
                ctx.restore();
            } catch {}
        }

        const vigGrad = ctx.createLinearGradient(x, y, x, y + THUMB_H);
        vigGrad.addColorStop(0, "rgba(0,0,0,0)");
        vigGrad.addColorStop(0.6, "rgba(0,0,0,0.15)");
        vigGrad.addColorStop(1, "rgba(0,0,0,0.65)");
        ctx.fillStyle = vigGrad;
        roundRect(ctx, x, y, THUMB_W, THUMB_H, 4);
        ctx.fill();

        const cx = x + THUMB_W / 2, cy = y + THUMB_H / 2;
        ctx.fillStyle = "rgba(0,0,0,0.55)";
        ctx.beginPath();
        ctx.arc(cx, cy, 20, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = "#ffffff";
        ctx.beginPath();
        ctx.moveTo(cx - 7, cy - 10);
        ctx.lineTo(cx - 7, cy + 10);
        ctx.lineTo(cx + 12, cy);
        ctx.closePath();
        ctx.fill();

        ctx.fillStyle = "#c0170f";
        roundRect(ctx, x + 5, y + 5, 24, 20, 3);
        ctx.fill();
        ctx.fillStyle = "#fff";
        ctx.font      = "bold 12px sans-serif";
        ctx.textAlign = "center";
        ctx.fillText(String(i + 1), x + 17, y + 19);
        ctx.textAlign = "left";

        const dur = r.duration || "";
        if (dur) {
            const dW = ctx.measureText(dur).width + 10;
            ctx.fillStyle = "rgba(0,0,0,0.82)";
            roundRect(ctx, x + THUMB_W - dW - 5, y + THUMB_H - 22, dW, 17, 3);
            ctx.fill();
            ctx.fillStyle = "#ffffff";
            ctx.font      = "bold 11px sans-serif";
            ctx.fillText(dur, x + THUMB_W - dW - 1, y + THUMB_H - 9);
        }

        const qual = r.quality || "";
        if (qual) {
            const qW = ctx.measureText(qual).width + 10;
            ctx.fillStyle = QUALITY_COLOR(qual);
            roundRect(ctx, x + 5, y + THUMB_H - 22, qW, 17, 3);
            ctx.fill();
            ctx.fillStyle = "#fff";
            ctx.font      = "bold 10px sans-serif";
            ctx.fillText(qual, x + 9, y + THUMB_H - 9);
        }

        ctx.fillStyle = "#1c1c1c";
        ctx.fillRect(x, y + THUMB_H, THUMB_W, INFO_H);
        ctx.fillStyle = "#2a2a2a";
        ctx.fillRect(x, y + THUMB_H + INFO_H - 1, THUMB_W, 1);

        ctx.fillStyle = "#e8e8e8";
        ctx.font      = "12px sans-serif";
        wrapText(ctx, r.title || "─", x + 5, y + THUMB_H + 15, THUMB_W - 10, 15, 2);

        ctx.fillStyle = "#777777";
        ctx.font      = "10px sans-serif";
        ctx.fillText(
            [r.views ? `👁 ${r.views}` : "", r.rating ? `⭐ ${r.rating}` : ""].filter(Boolean).join("  "),
            x + 5, y + THUMB_H + 46
        );
    }

    const fGrad = ctx.createLinearGradient(0, H - FOOTER_H, W, H);
    fGrad.addColorStop(0, "#8c0f09");
    fGrad.addColorStop(1, "#c0170f");
    ctx.fillStyle = fGrad;
    ctx.fillRect(0, H - FOOTER_H, W, FOOTER_H);

    ctx.fillStyle = "#ffffff";
    ctx.font      = "bold 13px sans-serif";
    ctx.fillText("⬆ Reply 1–8 to stream", PAD_X, H - FOOTER_H + 23);

    ctx.fillStyle = "rgba(255,255,255,0.55)";
    ctx.font      = "11px sans-serif";
    ctx.textAlign = "right";
    ctx.fillText("xncdi", W - PAD_X, H - FOOTER_H + 23);
    ctx.textAlign = "left";

    return canvas;
}

module.exports = {
    config: {
        name:        "xncdi",
        aliases:     ["xvideo", "xnxx", "xv"],
        version:     "6.0.0",
        author:      "SIFAT",
        countDown:   15,
        role:        1,
        category:    "18+",
        description: { en: "Search & stream 18+ videos from xncdi" },
        guide:       { en: "{pn} [keyword]\n{pn} trending [category]" },
    },

    onStart: async function ({ args, event, message, api: botApi, commandName }) {
        const { senderID } = event;

        if (!args[0]) {
            react(botApi, event, "❌");
            return message.reply(
                "🔞 xɴᴄᴅɪ — ᴜꜱᴀɢᴇ\n" +
                "━━━━━━━━━━━━━━━━\n" +
                "◈ .xncdi [keyword]\n" +
                "◈ .xncdi trending\n" +
                "◈ .xncdi trending bangla\n" +
                "◈ .xncdi trending hentai"
            );
        }

        const isTrending = args[0].toLowerCase() === "trending";
        const query      = isTrending ? args.slice(1).join(" ").trim() : args.join(" ").trim();

        react(botApi, event, "🔍");

        try {
            let results;
            if (isTrending) {
                const { data } = await fetchRetry(`${BASE}/trending`, query ? { category: query } : {});
                results = (data.results || data || []).slice(0, 8);
            } else {
                const { data } = await fetchRetry(`${BASE}/search`, { q: query, page: 1, sort: "relevance" });
                results = (data.results || []).slice(0, 8);
            }

            if (!results.length) {
                react(botApi, event, "❌");
                return message.reply(`⌀ ɴᴏ ʀᴇꜱᴜʟᴛꜱ ꜰᴏʀ "${query || "trending"}"`);
            }

            react(botApi, event, "✅");

            const imgPath = path.join(TMP_DIR, `xncdi_list_${Date.now()}.jpg`);
            let sent;

            const card = await makeXncdiCard(results, query, isTrending);
            if (card) {
                await fs.writeFile(imgPath, card.toBuffer("image/jpeg", { quality: 0.92 }));
                sent = await message.reply({ body: "", attachment: fs.createReadStream(imgPath) });
                setTimeout(() => fs.unlink(imgPath).catch(() => {}), 30_000);
            } else {
                sent = await message.reply(
                    `🔞 ${isTrending ? "Trending" : `"${truncate(query, 30)}"`}\n` +
                    `━━━━━━━━━━━━━━━━\n\n` +
                    results.map((r, i) =>
                        `[${i + 1}] ${truncate(r.title)}\n    ⏱ ${r.duration || "?"} │ 👁 ${r.views || "?"} │ ${r.quality || "?"}`
                    ).join("\n\n") +
                    `\n\n━━━━━━━━━━━━━━━━\n◈ ʀᴇᴘʟʏ 1-${results.length} ᴛᴏ ꜱᴛʀᴇᴀᴍ`
                );
            }

            if (sent?.messageID) {
                global.GoatBot.onReply.set(sent.messageID, {
                    commandName,
                    messageID: sent.messageID,
                    author:    senderID,
                    results,
                });
            }

        } catch (err) {
            react(botApi, event, "❌");
            return message.reply(`⌀ ꜰᴀɪʟᴇᴅ: ${err.message}`);
        }
    },

    onReply: async function ({ event, Reply, message, api: botApi }) {
        if (event.senderID !== Reply.author) return;

        const num = parseInt(event.body?.trim());
        if (isNaN(num) || num < 1 || num > Reply.results.length) {
            return message.reply(`⌀ ᴇɴᴛᴇʀ 1–${Reply.results.length}`);
        }

        react(botApi, event, "📥");

        try { botApi.unsendMessage(Reply.messageID); } catch {}
        global.GoatBot.onReply.delete(Reply.messageID);

        const selected = Reply.results[num - 1];

        try {
            const { data } = await fetchRetry(`${BASE}/video-source`, { url: selected.url }, 35000);

            const streamUrl =
                data.best || data.hq || data.lq ||
                Object.values(data.qualities || {})[0] || null;

            if (!streamUrl) {
                react(botApi, event, "❌");
                return message.reply("⌀ ɴᴏ ꜱᴛʀᴇᴀᴍ ᴜʀʟ ꜰᴏᴜɴᴅ");
            }

            const tmpPath = path.join(TMP_DIR, `xncdi_${Date.now()}.mp4`);
            const writer  = fs.createWriteStream(tmpPath);
            const res     = await axios({
                method: "get", url: streamUrl, responseType: "stream",
                timeout: 120000,
            });

            await new Promise((resolve, reject) => {
                res.data.pipe(writer);
                writer.on("finish", resolve);
                writer.on("error", reject);
                res.data.on("error", reject);
            });

            const stat = fs.statSync(tmpPath);
            if (stat.size < 1024) {
                fs.unlink(tmpPath).catch(() => {});
                react(botApi, event, "❌");
                return message.reply("⌀ ᴇᴍᴘᴛʏ ꜰɪʟᴇ — ʀᴇᴛʀʏ");
            }

            react(botApi, event, "✅");
            await message.reply({ body: "", attachment: fs.createReadStream(tmpPath) });
            setTimeout(() => fs.unlink(tmpPath).catch(() => {}), 30_000);

        } catch (err) {
            react(botApi, event, "❌");
            return message.reply(`⌀ ꜱᴛʀᴇᴀᴍ ꜰᴀɪʟᴇᴅ: ${err.message}`);
        }
    },
};

View raw file