← back to browse
goat command

song

No description

0
views
0
likes
0
installs
raw source
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
        );
    }
}

View raw file