← back to browse
goat command

yt

Search & download YouTube videos interactively

0
views
0
likes
0
installs

Aliases: youtube, yts

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

let pendingDownloads = {}; // Track results per thread

module.exports = {
  config: {
    name: "yt",
    aliases: ["youtube", "yts"],
    version: "6.6",
    author: "Aminul Sardar",
    role: 0,
    category: "media",
    shortDescription: "Search & download YouTube videos interactively",
    longDescription: "Search YouTube, show top results, then reply with a number to download the video.",
    guide: {
      en: "{pn} [search term] - Search YouTube\nReply 1-5 to download the video",
      bn: "{pn} [সার্চ শব্দ] - ইউটিউব সার্চ\n1-5 নম্বর রিপ্লাই করলে ভিডিও ডাউনলোড হবে"
    }
  },

  onStart: async function({ api, event, args }) {
    const threadID = event.threadID;
    const messageID = event.messageID;

    const query = args.join(" ").trim();
    if (!query) return api.sendMessage("❌ Please provide a search term!", threadID, messageID);

    try {
      // Fixed: Using correct search API endpoint
      const res = await axios.get(`https://aminul-youtube-api.vercel.app/search?query=${encodeURIComponent(query)}`);
      const data = res.data;

      if (!data || data.length === 0) {
        return api.sendMessage("😔 No videos found! Try another keyword.", threadID, messageID);
      }

      const videos = data.slice(0, 5); // Top 5 results
      pendingDownloads[threadID] = videos;

      // Build message with thumbnails
      let attachments = [];
      let msg = `🎬 **YouTube Search Results** 🎬\n🔎 **Query:** ${query}\n\n`;
      msg += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;

      for (let i = 0; i < videos.length; i++) {
        const v = videos[i];
        msg += `✨ **${i + 1}. ${v.title}**\n`;
        msg += `👤 Channel: ${v.author?.name || "Unknown"}\n`;
        msg += `⏱ Duration: ${v.duration?.timestamp || "N/A"}\n`;
        msg += `👁 Views: ${v.views?.toLocaleString() || "N/A"}\n`;
        msg += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;

        // Download thumbnail if available
        if (v.thumbnail) {
          try {
            const thumbPath = await downloadImage(v.thumbnail, `thumb_${threadID}_${i}.jpg`);
            attachments.push(fs.createReadStream(thumbPath));
          } catch (thumbErr) {
            console.error("Thumbnail download error:", thumbErr);
          }
        }
      }

      msg += `\n📥 Reply with **1-${videos.length}** to download your chosen video!\n⚡ Powered by Aminul API`;

      return api.sendMessage({ 
        body: msg, 
        attachment: attachments.length > 0 ? attachments : null 
      }, threadID, messageID);

    } catch (err) {
      console.error("Search error:", err);
      api.sendMessage("⚠️ Failed to fetch YouTube videos. Try again later.", threadID, messageID);
    }
  },

  onChat: async function({ api, event }) {
    const threadID = event.threadID;
    const messageID = event.messageID;
    const message = event.body?.trim();

    if (!message || !pendingDownloads[threadID] || !/^[1-5]$/.test(message)) return;

    const index = parseInt(message) - 1;
    const video = pendingDownloads[threadID][index];
    
    if (!video) {
      return api.sendMessage("❌ Invalid selection!", threadID, messageID);
    }

    const title = video.title;
    const url = video.url;

    // Send initial loading message
    const loadingMsg = await api.sendMessage(`⏳ Downloading **${title}**... Please wait!`, threadID);

    try {
      // Fixed: Using correct downloader API endpoint
      const downloadInfo = await axios.get(`https://aminul-rest-api-three.vercel.app/downloader/alldownloader?url=${encodeURIComponent(url)}`);
      
      // Fixed: Correct data path from API response
      const videoData = downloadInfo.data?.data?.data;
      if (!videoData) {
        throw new Error("Invalid API response structure");
      }

      const videoUrl = videoData.high || videoData.low;
      if (!videoUrl) {
        return api.sendMessage("❌ Cannot download this video. It may be restricted.", threadID, messageID);
      }

      // Create cache directory if it doesn't exist
      const cacheDir = path.join(__dirname, "cache");
      await fs.ensureDir(cacheDir);

      const filePath = path.join(cacheDir, `yt_${threadID}_${Date.now()}.mp4`);

      await downloadFile(videoUrl, filePath);

      await api.sendMessage({
        body: `✅ **Download Successful!**\n\n🎬 **Title:** ${title}\n📥 Your video is ready!`,
        attachment: fs.createReadStream(filePath)
      }, threadID, () => {
        // Clean up file after sending
        fs.unlinkSync(filePath);
      });

      // Clean up thumbnails
      for (let i = 0; i < 5; i++) {
        const thumbPath = path.join(__dirname, `thumb_${threadID}_${i}.jpg`);
        if (fs.existsSync(thumbPath)) {
          fs.unlinkSync(thumbPath);
        }
      }

    } catch (err) {
      console.error("Download error:", err);
      api.sendMessage("❌ Error occurred while downloading the video. Try again later.", threadID, messageID);
    }

    // Clear pending downloads
    delete pendingDownloads[threadID];
  }
};

// Helper: download thumbnail
async function downloadImage(url, filename) {
  try {
    const response = await axios({
      method: 'GET',
      url: url,
      responseType: 'arraybuffer'
    });
    
    const filePath = path.join(__dirname, filename);
    await fs.writeFile(filePath, response.data);
    return filePath;
  } catch (error) {
    throw new Error(`Failed to download image: ${error.message}`);
  }
}

// Helper: download video file
function downloadFile(url, filepath) {
  return new Promise((resolve, reject) => {
    const file = fs.createWriteStream(filepath);
    
    https.get(url, (response) => {
      // Check for redirects
      if (response.statusCode === 301 || response.statusCode === 302) {
        const redirectUrl = response.headers.location;
        return downloadFile(redirectUrl, filepath).then(resolve).catch(reject);
      }

      response.pipe(file);
      
      file.on('finish', () => {
        file.close();
        resolve();
      });
      
    }).on('error', (err) => {
      // Clean up failed download
      if (fs.existsSync(filepath)) {
        fs.unlinkSync(filepath);
      }
      reject(err);
    });
  });
        }

View raw file