تخطَّ إلى المحتوى
ARAB CODEمجتمع المطورين العرب

ytdl

javascriptv1MITنُشر ١ أغسطس ٢٠٢٦· ♥ 0 · ⬇ 1 · 💬 0

تحميل من يوتيوب

// كود تحميل يوتيوب
// https://whatsapp.com/channel/0029Vb7Nq294Y9le1aAcTE0D
// تابعو القناة هننشر اكواد تانية "izana,uncel shawaza" 
import { generateWAMessageFromContent } from '@whiskeysockets/baileys'
import axios from 'axios'
import fs from 'fs/promises'
import { createWriteStream } from 'fs'
import os from 'os'
import path from 'path'
import crypto from 'crypto'
import { spawn } from 'child_process'
import { pipeline } from 'stream/promises'

const NEW_API_BASE = 'https://engez.a7a.online/api/v1/download/ytdl'
const OLD_API_BASE = 'https://engez.a7a.online/api/v1/download/youtube'
const SELECT_SEPARATOR = '|'

const DOWNLOAD_TIMEOUT_MS = 120 * 1000
const TITLE_TIMEOUT_MS = 8 * 1000

const VIDEO_QUALITIES = ['144', '240', '360', '480', '720', '1080', '1440', '2160']
const AUDIO_QUALITIES = ['128', '320']

function addProtocol(url) {
  if (/^https?:\/\//i.test(url)) return url
  return `https://${url}`
}

function extractTargetUrl(input) {
  try {
    const normalized = addProtocol(input)
    const parsed = new URL(normalized)
    const host = parsed.hostname.replace(/^www\./i, '').replace(/^m\./i, '')

    if (host === 'engez.a7a.online' && (parsed.pathname.includes('/api/v1/download/youtube') || parsed.pathname.includes('/api/v1/download/ytdl'))) {
      const innerUrl = parsed.searchParams.get('url')
      if (innerUrl) return decodeURIComponent(innerUrl)
    }
  } catch {}

  return input
}

function isYouTubeUrl(input) {
  try {
    const normalized = addProtocol(input)
    const parsed = new URL(normalized)
    const host = parsed.hostname.replace(/^www\./i, '').replace(/^m\./i, '')

    return (
      host === 'youtu.be' ||
      host === 'youtube.com' ||
      host.endsWith('.youtube.com')
    )
  } catch {
    return false
  }
}

function buildNewApiUrl(url, type, quality) {
  const params = new URLSearchParams({ url })
  if (type) params.set('type', type)
  if (quality) params.set('quality', quality)
  return `${NEW_API_BASE}?${params.toString()}`
}

function buildOldApiUrl(url, type, quality) {
  const params = new URLSearchParams({ url })
  if (type) params.set('type', type)
  if (quality) params.set('quality', quality)
  return `${OLD_API_BASE}?${params.toString()}`
}

async function fetchTitleSafely(url) {
  try {
    const oembedUrl = `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`
    const { data } = await axios.get(oembedUrl, { timeout: TITLE_TIMEOUT_MS })
    return data?.title || null
  } catch (e) {
    console.error('fetchTitleSafely error:', e?.message || e)
    return null
  }
}

// New API: type is 'video' | 'audio'. Returns response.* on success.
async function fetchFromNewApi(url, type, quality) {
  const { data } = await axios.get(buildNewApiUrl(url, type, quality), {
    timeout: DOWNLOAD_TIMEOUT_MS
  })

  if (!data || data.success !== true || !data.response) {
    throw new Error(data?.error || 'تعذر تحميل هذا الاختيار من المصدر الرئيسي')
  }

  const r = data.response
  return {
    title: r.title || null,
    thumbnail: r.thumbnail || null,
    download_url: r.download_url,
    type: r.type === 'audio' ? 'audio' : 'mp4',
    requested_quality: r.requested_quality || quality || null,
    file_size_bytes: r.file_size_bytes || null,
    source_used: 'new'
  }
}

// Old API: type is 'video' | 'audio' (or 'mp4'/'mp3' depending on caller). Returns data.* on success.
async function fetchFromOldApi(url, type, quality) {
  const { data } = await axios.get(buildOldApiUrl(url, type, quality), {
    timeout: DOWNLOAD_TIMEOUT_MS
  })

  if (!data || data.success !== true) {
    throw new Error(data?.error || 'تعذر تحميل هذا الاختيار من المصدر الاحتياطي')
  }

  const d = data.data
  return {
    title: d.title || null,
    thumbnail: d.thumbnail || null,
    download_url: d.download_url,
    type: d.type === 'mp3' || d.type === 'audio' ? 'audio' : 'mp4',
    requested_quality: d.requested_quality || quality || null,
    file_size_bytes: d.file_size_bytes || null,
    source_used: 'old',
    is_fallback: true
  }
}

// Tries the new API first; falls back to the old API on any failure.
// `type` here is 'video' | 'audio' | null(auto). Quality is passed through as-is to both APIs,
// since the new API supports the same video/audio quality values as the old one.
async function fetchDownload(url, type, quality) {
  try {
    return await fetchFromNewApi(url, type, quality)
  } catch (e) {
    console.error('new API failed, falling back to old API:', e?.message || e)
    return await fetchFromOldApi(url, type, quality)
  }
}

async function downloadToFile(fileUrl, filePath) {
  const response = await axios.get(fileUrl, {
    responseType: 'stream',
    timeout: 120000,
    maxRedirects: 5,
    headers: {
      'User-Agent': 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36',
      Accept: '*/*',
      'Accept-Language': 'en-US,en;q=0.9'
    }
  })

  await pipeline(response.data, createWriteStream(filePath))
}

function runFfmpeg(args) {
  return new Promise((resolve, reject) => {
    const ff = spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] })
    let err = ''

    ff.stderr.on('data', (chunk) => {
      err += chunk.toString()
    })

    ff.on('error', reject)

    ff.on('close', (code) => {
      if (code === 0) return resolve()
      reject(new Error(`ffmpeg exited with code ${code}\n${err}`))
    })
  })
}

async function repairVideoWithFfmpeg(inputPath, outputPath) {
  try {
    await runFfmpeg([
      '-y',
      '-i', inputPath,
      '-fflags', '+genpts',
      '-movflags', '+faststart',
      '-map', '0:v:0?',
      '-map', '0:a:0?',
      '-c:v', 'libx264',
      '-preset', 'veryfast',
      '-crf', '23',
      '-c:a', 'aac',
      '-b:a', '128k',
      '-pix_fmt', 'yuv420p',
      outputPath
    ])
    return outputPath
  } catch (e) {
    console.error('repairVideoWithFfmpeg re-encode failed:', e?.message || e)

    await runFfmpeg([
      '-y',
      '-i', inputPath,
      '-c', 'copy',
      '-movflags', '+faststart',
      outputPath
    ])

    return outputPath
  }
}

async function convertAudioWithFfmpeg(inputPath, outputPath) {
  try {
    await runFfmpeg([
      '-y',
      '-i', inputPath,
      '-vn',
      '-c:a', 'libmp3lame',
      '-b:a', '192k',
      outputPath
    ])
    return outputPath
  } catch (e) {
    console.error('convertAudioWithFfmpeg failed:', e?.message || e)
    await runFfmpeg([
      '-y',
      '-i', inputPath,
      '-vn',
      '-c:a', 'aac',
      '-b:a', '128k',
      outputPath
    ])
    return outputPath
  }
}

async function prepareMediaFile(payload) {
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ytdl-'))
  const id = crypto.randomBytes(6).toString('hex')

  const srcPath = path.join(tmpDir, `source-${id}.bin`)
  const videoPath = path.join(tmpDir, `video-${id}.mp4`)
  const audioPath = path.join(tmpDir, `audio-${id}.mp3`)

  await downloadToFile(payload.download_url, srcPath)

  if (payload.type === 'mp4') {
    try {
      await repairVideoWithFfmpeg(srcPath, videoPath)
      return { filePath: videoPath, tmpDir, mimetype: 'video/mp4' }
    } catch (e) {
      console.error('video ffmpeg failed, sending original file:', e?.message || e)
      return { filePath: srcPath, tmpDir, mimetype: 'video/mp4' }
    }
  }

  try {
    await convertAudioWithFfmpeg(srcPath, audioPath)
    return { filePath: audioPath, tmpDir, mimetype: 'audio/mpeg' }
  } catch (e) {
    console.error('audio ffmpeg failed, sending original file:', e?.message || e)
    return { filePath: srcPath, tmpDir, mimetype: 'audio/mpeg' }
  }
}

async function sendDownloadedMedia(conn, chat, payload, quoted) {
  const isVideo = payload.type === 'mp4'
  const fallbackNote = payload.is_fallback ? '\nملحوظة: تم استخدام مصدر بديل' : ''
  const title = payload.title || 'بدون عنوان'

  let prepared
  try {
    prepared = await prepareMediaFile(payload)
    const buffer = await fs.readFile(prepared.filePath)

    if (isVideo) {
      await conn.sendMessage(
        chat,
        {
          video: buffer,
          mimetype: 'video/mp4',
          caption: `تم التحميل بنجاح\nالعنوان: ${title}\nالجودة: ${payload.requested_quality || 'auto'}\nالمصدر: ${payload.source_used || 'unknown'}${fallbackNote}`
        },
        { quoted }
      )
    } else {
      await conn.sendMessage(
        chat,
        {
          audio: buffer,
          mimetype: 'audio/mpeg',
          ptt: false
        },
        { quoted }
      )

      await conn.sendMessage(
        chat,
        {
          text: `تم التحميل بنجاح\nالعنوان: ${title}\nالجودة: ${payload.requested_quality || 'auto'}\nالمصدر: ${payload.source_used || 'unknown'}${fallbackNote}`
        },
        { quoted }
      )
    }
  } catch (e) {
    console.error('sendDownloadedMedia error:', e)
    const status = e?.response?.status
    const statusText = e?.response?.statusText
    const reason = status
      ? `الخادم رفض الطلب (${status}${statusText ? ' - ' + statusText : ''})`
      : (e?.code || e?.message || 'سبب غير معروف')

    await conn.sendMessage(
      chat,
      {
        text: `فشل تحميل الملف.\n\nالسبب: ${reason}\n\nحاول تختار جودة تانية أو أرسل الرابط من جديد.`
      },
      { quoted }
    )
  } finally {
    if (prepared?.tmpDir) {
      await fs.rm(prepared.tmpDir, { recursive: true, force: true }).catch(() => {})
    }
  }
}

function buildQualityRows(usedPrefix, command, url) {
  const quickRow = [
    {
      header: 'سريع',
      title: 'تحميل سريع (افتراضي)',
      description: 'يسيب السيرفر يختار أسرع جودة ومصدر متاح',
      id: `${usedPrefix}${command} ${url}${SELECT_SEPARATOR}auto${SELECT_SEPARATOR}auto`
    }
  ]

  const videoRows = VIDEO_QUALITIES.map((q) => ({
    header: 'فيديو',
    title: `${q}p`,
    description: `تحميل فيديو بجودة ${q}p`,
    id: `${usedPrefix}${command} ${url}${SELECT_SEPARATOR}video${SELECT_SEPARATOR}${q}`
  }))

  const audioRows = AUDIO_QUALITIES.map((q) => ({
    header: 'صوت',
    title: `${q}kbps`,
    description: `تحميل صوت بجودة ${q}kbps`,
    id: `${usedPrefix}${command} ${url}${SELECT_SEPARATOR}audio${SELECT_SEPARATOR}${q}`
  }))

  return { quickRow, videoRows, audioRows }
}

async function sendQualityList(conn, chat, quoted, usedPrefix, command, url, title) {
  const { quickRow, videoRows, audioRows } = buildQualityRows(usedPrefix, command, url)
  const titleLine = title ? `\nالعنوان: ${title}` : ''

  const contentMsg = {
    contentText: `اختر طريقة التحميل:${titleLine}`,
    footerText: 'اختر من القايمة تحت للتحميل:',
    buttons: [
      {
        buttonId: `${usedPrefix}تحميل_قايمة`,
        buttonText: { displayText: 'قايمة الجودات' },
        type: 1,
        nativeFlowInfo: {
          name: 'single_select',
          paramsJson: JSON.stringify({
            title: 'تحميل يوتيوب',
            sections: [
              {
                title: 'تحميل سريع',
                highlight_label: 'الأسرع',
                rows: quickRow
              },
              {
                title: 'جودات الفيديو',
                highlight_label: `${videoRows.length} خيار`,
                rows: videoRows
              },
              {
                title: 'جودات الصوت',
                highlight_label: `${audioRows.length} خيار`,
                rows: audioRows
              }
            ]
          })
        }
      }
    ],
    headerType: 1
  }

  const waMsg = generateWAMessageFromContent(chat, { buttonsMessage: contentMsg }, { quoted })
  await conn.relayMessage(chat, waMsg.message, { messageId: waMsg.key.id })
}

const handler = async (m, { conn, args, usedPrefix, command }) => {
  const rawInput = args.join(' ').trim()

  if (!rawInput) {
    return conn.sendMessage(
      m.chat,
      {
        text: `تحميل من يوتيوب\n\nأرسل الأمر مع الرابط:\n${usedPrefix}${command} <رابط يوتيوب>\n\nمثال:\n${usedPrefix}${command} https://www.youtube.com/watch?v=xxxx`
      },
      { quoted: m }
    )
  }

  const selectedInput = extractTargetUrl(rawInput)
  const parts = selectedInput.split(SELECT_SEPARATOR)
  const isSelection = parts.length === 3

  const url = isSelection ? parts[0].trim() : selectedInput
  const selectedType = isSelection ? parts[1].trim() : null
  const selectedQuality = isSelection ? parts[2].trim() : null
  const isAuto = selectedType === 'auto'

  if (!isYouTubeUrl(url)) {
    return conn.sendMessage(
      m.chat,
      { text: 'الرابط ده مش رابط يوتيوب صحيح.' },
      { quoted: m }
    )
  }

  try {
    if (isSelection) {
      await conn.sendMessage(
        m.chat,
        { text: 'جاري التحميل، ممكن ياخد شوية وقت حسب طول الفيديو والمصدر المتاح...' },
        { quoted: m }
      )

      const payload = await fetchDownload(url, isAuto ? null : selectedType, isAuto ? null : selectedQuality)
      await sendDownloadedMedia(conn, m.chat, payload, m)
      return
    }

    await conn.sendMessage(m.chat, { text: 'جاري البحث عن الفيديو...' }, { quoted: m })
    const title = await fetchTitleSafely(url)
    await sendQualityList(conn, m.chat, m, usedPrefix, command, url, title)
  } catch (e) {
    console.error('يوتيوب handler error:', e)
    const msg = e?.message || 'حدث خطأ غير متوقع'
    await conn.sendMessage(
      m.chat,
      { text: `${msg}\n\nتأكد من صحة الرابط وحاول مرة أخرى.` },
      { quoted: m }
    )
  }
}

handler.command = /^(يوتيوب|ytdl|يوت)$/i
handler.help = ['يوتيوب <رابط>']
handler.tags = ['downloader']

export default handler

التعليقات (0)

سجّل الدخول للتعليق.