مانهوا
javascriptv1MITنُشر ١ أغسطس ٢٠٢٦· ♥ 0 · ⬇ 1 · 💬 0
تحميل وبحث فصول مانهوا
// كود بحث وتحميل فصول مانهوا
// https://whatsapp.com/channel/0029Vb7Nq294Y9le1aAcTE0D
// تابعو القناة هننشر اكواد تانية "izana,uncel shawaza"
import { generateWAMessageFromContent, prepareWAMessageMedia } from '@whiskeysockets/baileys'
import axios from 'axios'
import crypto from 'crypto'
import fs from 'fs-extra'
import path from 'path'
import os from 'os'
import PDFDocument from 'pdfkit'
import sharp from 'sharp'
const API_BASE = 'https://engez.a7a.online/api/v1'
const MANHWA_ENDPOINT = `${API_BASE}/anime/manhwa`
const SELECT_SEPARATOR = '|'
const CACHE_TTL_MS = 3 * 60 * 1000
const DEFAULT_IMAGE = 'https://i.postimg.cc/bS01zQwK/upload-1767808833485.jpg'
const MAX_CHAPTERS_PER_MERGE = 15
const contextCache = new Map()
function makeToken() {
return crypto.randomBytes(8).toString('hex')
}
function now() {
return Date.now()
}
function isExpired(entry) {
return !entry || (now() - entry.timestamp) > CACHE_TTL_MS
}
function setContext(data) {
const token = makeToken()
contextCache.set(token, { ...data, timestamp: now() })
return token
}
function getContext(token) {
const entry = contextCache.get(token)
if (isExpired(entry)) {
contextCache.delete(token)
return null
}
return entry
}
function clearStaleContexts() {
const ts = now()
for (const [token, entry] of contextCache.entries()) {
if (!entry || (ts - entry.timestamp) > CACHE_TTL_MS) {
contextCache.delete(token)
}
}
}
function cleanText(text = '') {
return String(text)
.replace(/\s+/g, ' ')
.replace(/\s*[-–—:|]+\s*$/g, '')
.replace(/^\s*[-–—:|]+\s*/g, '')
.trim()
}
function safeFileName(text = '') {
return cleanText(text)
.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, ' ')
.trim()
}
// ─── genres راجعة كـ array من objects {id, name, series_count}، مش
// array نصوص بسيطة زي tags بتاعة الأنمي — لازم .name صريح
function genreNames(genres = []) {
return genres.map((g) => g.name).filter(Boolean).join('، ')
}
function describeSeriesItem(item, index) {
const title = cleanText(item.title || 'بدون عنوان')
const status = item.status || 'غير معروف'
const rating = item.rating != null ? Number(item.rating).toFixed(1) : 'غير معروف'
return {
header: `مانهوا #${index + 1}`,
title,
description: `الحالة: ${status} • التقييم: ${rating}`
}
}
function describeChapterItem(item, index) {
const chapterNum = item.chapter || String(index + 1)
const title = cleanText(item.title || `الفصل ${chapterNum}`)
return {
header: `فصل #${chapterNum}`,
title,
description: `👁️ ${item.views || 0} مشاهدة`
}
}
async function sendSelectList(conn, chat, quoted, { title, footer, sections, posterUrl }) {
let mediaMessage = null
try {
mediaMessage = await prepareWAMessageMedia(
{ image: { url: posterUrl || DEFAULT_IMAGE } },
{ upload: conn.waUploadToServer }
)
} catch {
try {
mediaMessage = await prepareWAMessageMedia(
{ image: { url: DEFAULT_IMAGE } },
{ upload: conn.waUploadToServer }
)
} catch {
mediaMessage = null
}
}
try {
const msgContent = {
viewOnceMessage: {
message: {
interactiveMessage: {
body: { text: title },
footer: { text: footer },
header: mediaMessage ? {
hasMediaAttachment: true,
imageMessage: mediaMessage.imageMessage
} : { hasMediaAttachment: false, title },
nativeFlowMessage: {
buttons: [{
name: 'single_select',
buttonParamsJson: JSON.stringify({
title: '🔏 مانهوا 🔏',
sections
})
}]
}
}
}
}
}
const waMsg = generateWAMessageFromContent(chat, msgContent, { userJid: conn.user.jid, quoted })
await conn.relayMessage(chat, waMsg.message, { messageId: waMsg.key.id })
} catch (e) {
let txt = `${title}\n\n`
for (const section of sections) {
for (const row of section.rows) {
txt += `${row.header}\n${row.title}\n${row.id}\n\n`
}
}
await conn.sendMessage(chat, { text: txt }, { quoted })
}
}
async function fetchSearchResults(query) {
clearStaleContexts()
const apiUrl = `${MANHWA_ENDPOINT}?action=${encodeURIComponent('بحث')}&q=${encodeURIComponent(query)}`
const { data } = await axios.get(apiUrl, { timeout: 30000 })
if (!data || data.success !== true) {
throw new Error('لم يتم العثور على نتائج لهذا البحث')
}
const results = data.response?.results
if (!Array.isArray(results) || results.length === 0) {
throw new Error('لا توجد نتائج مطابقة')
}
return results
}
async function fetchChapters(seriesId) {
clearStaleContexts()
const apiUrl = `${MANHWA_ENDPOINT}?action=${encodeURIComponent('فصول')}&id=${encodeURIComponent(seriesId)}`
const { data } = await axios.get(apiUrl, { timeout: 30000 })
if (!data || data.success !== true) {
throw new Error('لم يتم العثور على فصول لهذه المانهوا')
}
const results = data.response?.results
if (!Array.isArray(results) || results.length === 0) {
throw new Error('لا توجد فصول متاحة لهذه المانهوا')
}
return results
}
// ─── بيرجّع الفصل الكامل (بما فيه الصور)، بعكس fetchChapters اللي
// بترجّع بيانات وصفية بس من غير images (زي ما تأكدنا من استجابة الـ API)
async function fetchChapterFull(chapterId) {
const apiUrl = `${MANHWA_ENDPOINT}?action=${encodeURIComponent('فصل')}&id=${encodeURIComponent(chapterId)}`
const { data } = await axios.get(apiUrl, { timeout: 30000 })
if (!data || data.success !== true || !data.response) {
throw new Error('لم يتم العثور على هذا الفصل')
}
const chapter = data.response
if (!Array.isArray(chapter.images) || chapter.images.length === 0) {
throw new Error('لا توجد صور في هذا الفصل')
}
return chapter
}
// ─── تحويل مجموعة فصول لملف PDF واحد، كل صورة صفحة، مع ضغط jpeg
// (نفس منطق الهاندلر الأصلي بالظبط: sharp + pdfkit)
async function createChaptersPDF(chapters, outputPath) {
const doc = new PDFDocument({ autoFirstPage: false, margin: 0 })
const stream = fs.createWriteStream(outputPath)
doc.pipe(stream)
let addedPages = 0
for (const chap of chapters) {
// images راجعة كـ [{image, order}, ...] — نرتبها بالـ order احتياطاً
// (الاستجابة الفعلية جات مرتبة أصلاً، لكن منعتمدش على ترتيب المصدر)
const sortedImages = [...(chap.images || [])].sort((a, b) => (a.order || 0) - (b.order || 0))
for (const img of sortedImages) {
try {
const res = await axios.get(img.image, {
responseType: 'arraybuffer',
timeout: 30000,
headers: { 'User-Agent': 'Mozilla/5.0' }
})
const buffer = await sharp(Buffer.from(res.data))
.jpeg({ quality: 45, mozjpeg: true })
.toBuffer()
const meta = await sharp(buffer).metadata()
if (!meta.width || !meta.height) continue
doc.addPage({ size: [meta.width, meta.height] })
doc.image(buffer, 0, 0, { width: meta.width, height: meta.height })
addedPages++
} catch (e) {
console.error(`Skipping image: ${img.image}`, e.message)
}
}
}
if (addedPages === 0) {
doc.destroy()
throw new Error('لم يتم إضافة أي صفحات للـ PDF، فشل تحميل كل الصور.')
}
doc.end()
return new Promise((resolve, reject) => {
stream.on('finish', resolve)
stream.on('error', reject)
})
}
async function sendSearchResults(conn, m, usedPrefix, query, results) {
const token = setContext({ type: 'search', query, results })
const rows = results.slice(0, 20).map((item, index) => {
const info = describeSeriesItem(item, index)
return {
header: info.header,
title: info.title,
description: info.description,
id: `${usedPrefix}مانهوا search${SELECT_SEPARATOR}${token}${SELECT_SEPARATOR}${index}`
}
})
const posterUrl = results[0]?.poster?.medium || results[0]?.poster?.thumbnail || DEFAULT_IMAGE
await sendSelectList(conn, m.chat, m, {
title: `*🔎 نتائج البحث عن:* ${query}`,
footer: 'اختر المانهوا المطلوبة من القائمة:',
posterUrl,
sections: [
{
title: '📥 نتائج البحث',
highlight_label: `📥 ${rows.length} نتيجة`,
rows
}
]
})
}
async function sendChaptersList(conn, m, usedPrefix, seriesTitle, chapters, seriesId, posterUrl) {
const token = setContext({
type: 'chapters',
seriesTitle,
seriesId,
chapters
})
const rows = chapters.slice(0, 30).map((item, index) => {
const info = describeChapterItem(item, index)
return {
header: info.header,
title: info.title,
description: info.description,
id: `${usedPrefix}مانهوا chapter${SELECT_SEPARATOR}${token}${SELECT_SEPARATOR}${index}`
}
})
await sendSelectList(conn, m.chat, m, {
title: `*📖 المانهوا:* ${seriesTitle}\n*📚 عدد الفصول:* ${chapters.length}`,
footer: 'اختر الفصل المطلوب من القائمة:',
posterUrl,
sections: [
{
title: '📥 الفصول المتاحة',
highlight_label: `📥 ${rows.length} فصل`,
rows
}
]
})
}
async function sendChapterPDF(conn, chat, quoted, { chapter, seriesTitle, usedPrefix }) {
const pdfPath = path.join(os.tmpdir(), `manhwa_${Date.now()}.pdf`)
try {
await createChaptersPDF([chapter], pdfPath)
} catch (e) {
throw new Error(`فشل إنشاء PDF: ${e.message}`)
}
const chapterLabel = cleanText(chapter.title || `الفصل ${chapter.chapter}`)
const { documentMessage } = await prepareWAMessageMedia(
{
document: { url: pdfPath },
mimetype: 'application/pdf',
fileName: `${safeFileName(seriesTitle)} - ${safeFileName(chapterLabel)}.pdf`
},
{ upload: conn.waUploadToServer }
)
const buttons = []
if (chapter.navigation?.prev) {
buttons.push({
name: 'quick_reply',
buttonParamsJson: JSON.stringify({
display_text: '⬅️ الفصل السابق',
id: `${usedPrefix}مانهوا chapterid${SELECT_SEPARATOR}${chapter.navigation.prev}`
})
})
}
if (chapter.navigation?.next) {
buttons.push({
name: 'quick_reply',
buttonParamsJson: JSON.stringify({
display_text: 'الفصل التالي ➡️',
id: `${usedPrefix}مانهوا chapterid${SELECT_SEPARATOR}${chapter.navigation.next}`
})
})
}
const msgContent = {
viewOnceMessage: {
message: {
interactiveMessage: {
body: {
text: `📖 مانهوا: *${seriesTitle}*\n🔖 الفصل: *${chapterLabel}*\n👁️ المشاهدات: ${chapter.views || 0}\n🖼️ عدد الصور: ${chapter.images.length}`
},
footer: { text: '🔏 AppSwat Manhwa 🔏' },
header: { hasMediaAttachment: true, documentMessage },
nativeFlowMessage: { buttons }
}
}
}
}
const waMsg = generateWAMessageFromContent(chat, msgContent, { userJid: conn.user.jid, quoted })
await conn.relayMessage(chat, waMsg.message, { messageId: waMsg.key.id })
fs.unlink(pdfPath, () => {})
}
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} solo leveling\n│\n│ أو اختر من القوائم بعد البحث.\n│\n╰────────────────╯`
},
{ quoted: m }
)
}
const parts = rawInput.split(SELECT_SEPARATOR)
const action = parts[0]?.trim()?.toLowerCase()
try {
if (action === 'search') {
const token = parts[1]
const index = parseInt(parts[2], 10)
const ctx = getContext(token)
if (!ctx || ctx.type !== 'search') {
return conn.sendMessage(
m.chat,
{ text: '❌ انتهت جلسة البحث، أعد إرسال اسم المانهوا.' },
{ quoted: m }
)
}
const selected = ctx.results[index]
if (!selected) {
return conn.sendMessage(
m.chat,
{ text: '❌ الاختيار غير موجود، أعد المحاولة.' },
{ quoted: m }
)
}
await conn.sendMessage(m.chat, { text: '⏳ جاري جلب الفصول...' }, { quoted: m })
const seriesId = selected.id
const seriesTitle = cleanText(selected.title || 'بدون عنوان')
const posterUrl = selected.poster?.medium || selected.poster?.thumbnail || DEFAULT_IMAGE
const chapters = await fetchChapters(seriesId)
await sendChaptersList(conn, m, usedPrefix, seriesTitle, chapters, seriesId, posterUrl)
return
}
if (action === 'chapter') {
const token = parts[1]
const index = parseInt(parts[2], 10)
const ctx = getContext(token)
if (!ctx || ctx.type !== 'chapters') {
return conn.sendMessage(
m.chat,
{ text: '❌ انتهت جلسة الفصول، أعد اختيار المانهوا.' },
{ quoted: m }
)
}
const chapterMeta = ctx.chapters[index]
if (!chapterMeta) {
return conn.sendMessage(
m.chat,
{ text: '❌ الفصل غير موجود، أعد المحاولة.' },
{ quoted: m }
)
}
await conn.sendMessage(m.chat, { text: '⏳ جاري تحميل صور الفصل وتجهيز PDF...' }, { quoted: m })
const chapter = await fetchChapterFull(chapterMeta.id)
await sendChapterPDF(conn, m.chat, m, {
chapter,
seriesTitle: cleanText(ctx.seriesTitle || 'بدون عنوان'),
usedPrefix
})
return
}
// اختيار مباشر بالـ id (زر "الفصل التالي/السابق" بعد ما استلمنا الـ
// PDF — هنا مفيش context قديم نرجعله، الـ id بييجي صريح في الأمر)
if (action === 'chapterid') {
const chapterId = parts[1]
if (!chapterId) {
return conn.sendMessage(
m.chat,
{ text: '❌ رقم الفصل غير صالح.' },
{ quoted: m }
)
}
await conn.sendMessage(m.chat, { text: '⏳ جاري تحميل صور الفصل وتجهيز PDF...' }, { quoted: m })
const chapter = await fetchChapterFull(chapterId)
const seriesTitle = cleanText(chapter.serie?.title || 'بدون عنوان')
await sendChapterPDF(conn, m.chat, m, { chapter, seriesTitle, usedPrefix })
return
}
await conn.sendMessage(m.chat, { text: '⏳ جاري البحث عن المانهوا...' }, { quoted: m })
const results = await fetchSearchResults(rawInput)
await sendSearchResults(conn, m, usedPrefix, rawInput, results)
} catch (e) {
console.error('manhwa handler error:', e)
const msg = e?.response?.data?.error || e?.message || 'حدث خطأ غير متوقع'
await conn.sendMessage(
m.chat,
{ text: `❌ ${msg}\n\nتأكد من الاسم وحاول مرة أخرى.` },
{ quoted: m }
)
}
}
handler.command = /^(مانهوا|manhwa|بحث_مانهوا|فصول_مانهوا)$/i
handler.help = ['مانهوا <اسم>']
handler.tags = ['downloader']
export default handler
التعليقات (0)
سجّل الدخول للتعليق.