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

youtube

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 API_BASE = 'https://engez.a7a.online/api/v1'
const YT_SEARCH = `${API_BASE}/search/youtube`
const YT_DOWNLOAD_V2 = `${API_BASE}/download/youtubev2`
const YT_DOWNLOAD_NEW = `${API_BASE}/download/ytdl`
const YT_DOWNLOAD_OLD = `${API_BASE}/download/youtube`

const SEP = '|'
const DOWNLOAD_TIMEOUT_MS = 120 * 1000

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

function normalizeDownloadUrl(value) {
    if (typeof value !== 'string') return null
    const trimmed = value.trim()
    if (!trimmed) return null
    return /^https?:\/\//i.test(trimmed) ? trimmed : null
}

function normalizePayload(payload) {
    return {
        title: payload?.title || null,
        thumbnail: payload?.thumbnail || null,
        download_url: normalizeDownloadUrl(payload?.download_url || payload?.downloadUrl),
        type: payload?.type === 'audio' || payload?.type === 'mp3' ? 'audio' : 'mp4',
        requested_quality: payload?.requested_quality || payload?.quality || null,
        file_size_bytes: payload?.file_size_bytes || payload?.fileSizeBytes || null,
        source_used: payload?.source_used || payload?.source || 'unknown',
        is_fallback: Boolean(payload?.is_fallback)
    }
}

// ============ بحث ============

async function searchYouTube(query) {
    try {
        const params = new URLSearchParams({ q: query })
        const response = await axios.get(`${YT_SEARCH}?${params.toString()}`, {
            timeout: 30000
        })

        if (!response.data?.success) {
            throw new Error(response.data?.error || 'فشل البحث')
        }

        return response.data.results || []
    } catch (error) {
        throw new Error(error?.response?.data?.error || error.message || 'فشل الاتصال')
    }
}

// ============ المصدر الأول (V2) - تحميل سريع ============

async function fetchFromV2(url, type) {
    const params = new URLSearchParams({ url, type })

    const { data } = await axios.get(`${YT_DOWNLOAD_V2}?${params.toString()}`, {
        timeout: DOWNLOAD_TIMEOUT_MS
    })

    if (!data?.success || !data.response) {
        throw new Error(data?.error || 'تعذر التحميل السريع')
    }

    const r = data.response
    const payload = normalizePayload({
        title: r.title,
        thumbnail: r.thumbnail,
        download_url: r.download_url || r.downloadUrl,
        type: r.type,
        requested_quality: r.requested_quality || r.quality,
        file_size_bytes: r.file_size_bytes || r.fileSizeBytes,
        source_used: r.source || 'youtubev2'
    })

    if (!payload.download_url) {
        throw new Error('API لم ترجع رابط تحميل صالح')
    }

    return payload
}

// ============ المصدر الاحتياطي (ytdl -> youtube) ============

function buildNewApiUrl(url, type, quality) {
    const params = new URLSearchParams({ url })
    if (type) params.set('type', type)
    if (quality) params.set('quality', quality)
    return `${YT_DOWNLOAD_NEW}?${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 `${YT_DOWNLOAD_OLD}?${params.toString()}`
}

async function fetchFromNewApi(url, type, quality) {
    const { data } = await axios.get(buildNewApiUrl(url, type, quality), {
        timeout: DOWNLOAD_TIMEOUT_MS
    })

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

    const r = data.response
    const payload = normalizePayload({
        title: r.title,
        thumbnail: r.thumbnail,
        download_url: r.download_url || r.downloadUrl,
        type: r.type,
        requested_quality: r.requested_quality || r.quality || quality,
        file_size_bytes: r.file_size_bytes || r.fileSizeBytes,
        source_used: r.source || 'ytdl'
    })

    if (!payload.download_url) {
        throw new Error('المصدر الرئيسي لم يرجع رابط تحميل صالح')
    }

    return payload
}

async function fetchFromOldApi(url, type, quality) {
    const { data } = await axios.get(buildOldApiUrl(url, type, quality), {
        timeout: DOWNLOAD_TIMEOUT_MS
    })

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

    const d = data.data || data.response || {}
    const payload = normalizePayload({
        title: d.title,
        thumbnail: d.thumbnail,
        download_url: d.download_url || d.downloadUrl,
        type: d.type,
        requested_quality: d.requested_quality || d.quality || quality,
        file_size_bytes: d.file_size_bytes || d.fileSizeBytes,
        source_used: d.source || 'youtube',
        is_fallback: true
    })

    if (!payload.download_url) {
        throw new Error('المصدر الاحتياطي لم يرجع رابط تحميل صالح')
    }

    return payload
}

async function fetchFromFallbackChain(url, type, quality) {
    try {
        return await fetchFromNewApi(url, type, quality)
    } catch (e) {
        console.error('ytdl فشل، بجرب youtube:', e?.message || e)
        return await fetchFromOldApi(url, type, quality)
    }
}

// ============ تجهيز وإرسال الميديا ============

async function downloadToFile(fileUrl, filePath) {
    const safeUrl = normalizeDownloadUrl(fileUrl)
    if (!safeUrl) {
        throw new Error('ERR_INVALID_URL')
    }

    const response = await axios.get(safeUrl, {
        responseType: 'stream',
        timeout: DOWNLOAD_TIMEOUT_MS,
        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 فشل:', 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 فشل:', e?.message || e)
        await runFfmpeg([
            '-y', '-i', inputPath,
            '-vn',
            '-c:a', 'aac',
            '-b:a', '128k',
            outputPath
        ])
        return outputPath
    }
}

async function prepareMediaFile(payload) {
    const safePayload = normalizePayload(payload)
    if (!safePayload.download_url) {
        throw new Error('API لم ترجع رابط تحميل صالح')
    }

    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(safePayload.download_url, srcPath)

    const isVideo = safePayload.type === 'mp4'

    if (isVideo) {
        try {
            await repairVideoWithFfmpeg(srcPath, videoPath)
            return { filePath: videoPath, tmpDir, mimetype: 'video/mp4' }
        } catch (e) {
            console.error('video ffmpeg فشل، هبعت الملف الأصلي:', 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 فشل، هبعت الملف الأصلي:', e?.message || e)
        return { filePath: srcPath, tmpDir, mimetype: 'audio/mpeg' }
    }
}

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

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

        if (isVideo) {
            await conn.sendMessage(chat, {
                video: buffer,
                mimetype: 'video/mp4',
                caption: `تم التحميل بنجاح\nالعنوان: ${title}\nالجودة: ${safePayload.requested_quality || 'auto'}\nالمصدر: ${safePayload.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الجودة: ${safePayload.requested_quality || 'auto'}\nالمصدر: ${safePayload.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(() => {})
        }
    }
}

// ============ الأزرار: صوت سريع / فيديو سريع / جودات احتياطية ============

async function sendQuickChoiceButtons(conn, chat, quoted, usedPrefix, command, videoId, title) {
    const titleLine = title ? `\nالعنوان: ${title}` : ''

    const buttons = [
        {
            buttonId: `${usedPrefix}${command} ${videoId}${SEP}quick${SEP}audio`,
            buttonText: { displayText: '🎵 صوت سريع' },
            type: 1
        },
        {
            buttonId: `${usedPrefix}${command} ${videoId}${SEP}quick${SEP}video`,
            buttonText: { displayText: '🎬 فيديو سريع' },
            type: 1
        },
        {
            buttonId: `${usedPrefix}${command} ${videoId}${SEP}fallback`,
            buttonText: { displayText: '⚙️ جودات احتياطية' },
            type: 1
        }
    ]

    const contentMsg = {
        contentText: `اختر طريقة التحميل:${titleLine}`,
        footerText: 'صوت/فيديو سريع بيجيب أسرع جودة متاحة، وجودات احتياطية بتديك تحكم كامل',
        buttons,
        headerType: 1
    }

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

// ============ قايمة الجودات الطويلة ============

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

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

    return { videoRows, audioRows }
}

async function sendQualityList(conn, chat, quoted, usedPrefix, command, videoId, title) {
    const { videoRows, audioRows } = buildQualityRows(usedPrefix, command, videoId)
    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: `${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• ${usedPrefix}${command} <بحث>\n\nمثال:\n${usedPrefix}${command} https://youtube.com/watch?v=xxx\n${usedPrefix}${command} anime edit`
        }, { quoted: m })
    }

    const isUrl = /(?:youtube\.com|youtu\.be|m\.youtube\.com)/i.test(rawInput)
    const parts = rawInput.split(SEP)

    const isQuickChoice = parts.length === 3 && parts[1] === 'quick'
    const isQualityChoice = parts.length === 3 && (parts[1] === 'video' || parts[1] === 'audio')
    const isFallbackRequest = parts.length === 2 && parts[1] === 'fallback'
    const isSearchResultSelection = parts.length === 2 && parts[1] === 'select'

    if (isQuickChoice) {
        const [videoId, , type] = parts
        await conn.sendMessage(m.chat, { text: '⏳ جاري التحميل السريع...' }, { quoted: m })

        try {
            const payload = await fetchFromV2(`https://youtube.com/watch?v=${videoId}`, type)
            await sendDownloadedMedia(conn, m.chat, payload, m)
        } catch (e) {
            await conn.sendMessage(m.chat, { text: `❌ ${e.message}\n\nجرب "جودات احتياطية" بدل كده` }, { quoted: m })
        }
        return
    }

    if (isQualityChoice) {
        const [videoId, type, quality] = parts
        await conn.sendMessage(m.chat, { text: '⏳ جاري التحميل...' }, { quoted: m })

        try {
            const payload = await fetchFromFallbackChain(`https://youtube.com/watch?v=${videoId}`, type, quality)
            await sendDownloadedMedia(conn, m.chat, payload, m)
        } catch (e) {
            await conn.sendMessage(m.chat, { text: `❌ ${e.message}` }, { quoted: m })
        }
        return
    }

    if (isFallbackRequest) {
        const [videoId] = parts
        try {
            const title = await axios.get(`https://www.youtube.com/oembed?url=https://youtube.com/watch?v=${videoId}&format=json`)
                .then(r => r.data?.title || null)
                .catch(() => null)

            await sendQualityList(conn, m.chat, m, usedPrefix, command, videoId, title)
        } catch (e) {
            await conn.sendMessage(m.chat, { text: `❌ ${e.message}` }, { quoted: m })
        }
        return
    }

    if (isSearchResultSelection) {
        const [videoId] = parts
        try {
            const title = await axios.get(`https://www.youtube.com/oembed?url=https://youtube.com/watch?v=${videoId}&format=json`)
                .then(r => r.data?.title || null)
                .catch(() => null)

            await sendQuickChoiceButtons(conn, m.chat, m, usedPrefix, command, videoId, title)
        } catch (e) {
            await conn.sendMessage(m.chat, { text: `❌ ${e.message}` }, { quoted: m })
        }
        return
    }

    if (isUrl) {
        const url = rawInput
        await conn.sendMessage(m.chat, { text: '⏳ جاري جلب المعلومات...' }, { quoted: m })

        try {
            const videoId = url.match(/(?:v=|\/)([0-9A-Za-z_-]{11})(?:[&?]|$)/)?.[1]
            if (!videoId) throw new Error('رابط غير صحيح')

            const title = await axios.get(`https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`)
                .then(r => r.data?.title || null)
                .catch(() => null)

            await sendQuickChoiceButtons(conn, m.chat, m, usedPrefix, command, videoId, title)
        } catch (e) {
            await conn.sendMessage(m.chat, { text: `❌ ${e.message}` }, { quoted: m })
        }
        return
    }

    // بحث
    await conn.sendMessage(m.chat, { text: `🔍 جاري البحث عن: ${rawInput}` }, { quoted: m })

    try {
        const results = await searchYouTube(rawInput)

        if (results.length === 0) {
            return conn.sendMessage(m.chat, { text: '❌ لا توجد نتائج' }, { quoted: m })
        }

        const sections = [{
            title: '🎬 النتائج',
            rows: results.slice(0, 10).map((video) => ({
                title: (video.title || 'بدون عنوان').substring(0, 40),
                description: `👤 ${video.author || 'غير معروف'} | 👁️ ${video.views || '-'} | ⏱️ ${video.duration || '-'}`,
                id: `${usedPrefix}${command} ${video.id}${SEP}select`
            }))
        }]

        const msg = generateWAMessageFromContent(m.chat, {
            viewOnceMessage: {
                message: {
                    interactiveMessage: {
                        body: { text: `🔍 *نتائج البحث عن:* ${rawInput}\n📊 *عدد النتائج:* ${results.length}\n\n👇 اختر الفيديو للتحميل:` },
                        footer: { text: '🎬 YouTube Downloader' },
                        nativeFlowMessage: {
                            buttons: [{
                                name: 'single_select',
                                buttonParamsJson: JSON.stringify({
                                    title: '📋 اختر فيديو',
                                    sections
                                })
                            }]
                        }
                    }
                }
            }
        }, { userJid: conn.user.jid, quoted: m })

        await conn.relayMessage(m.chat, msg.message, { messageId: msg.key.id })
    } catch (e) {
        await conn.sendMessage(m.chat, { text: `❌ ${e.message}` }, { quoted: m })
    }
}

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

export default handler

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

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