{"repo": "boycoe7k/api-blacknodezw-", "files": {"api/index.js": {"4611056275b12f93bc9e6ae7b97d94f45aa0ceb4": {"t": 1785746702.6054657, "n": 80345, "h": "658a285de2dd9049", "c": "// api/index.js - Black Node ZW - All endpoints unified\nconst axios = require('axios');\nconst QRCode = require('qrcode');\n\nfunction cors(res) {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type,x-api-key,Authorization');\n}\nconst ok = (res, data) => res.status(200).json({ status: 200, data });\nconst err = (res, msg, code = 500) => res.status(code).json({ status: code, error: msg });\n\n// ── FIREBASE ──────────────────────────────────────────────────────────────────\nlet _db = null;\nasync function getDB() {\n if (_db) return _db;\n const { initializeApp, getApps, cert } = require('firebase-admin/app');\n const { getFirestore } = require('firebase-admin/firestore');\n if (!getApps().length) {\n if (process.env.FIREBASE_SERVICE_ACCOUNT) {\n initializeApp({ credential: cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT)) });\n } else { initializeApp({ projectId: 'apiblacknodezw' }); }\n }\n _db = getFirestore();\n return _db;\n}\n\n// NOTE: Authentication / API-key checks have been removed. This backend is\n// now fully open - anyone can call any endpoint directly, no key required.\n\n// ── HELPERS ───────────────────────────────────────────────────────────────────\n// Extract YouTube video ID from any YouTube URL including Shorts\nfunction getYTId(url) {\n return url.match(/(?:v=|youtu\\.be\\/|embed\\/|shorts\\/|\\/v\\/)([^&\\n?#]+)/)?.[1] || null;\n}\n\n// Cobalt download helper with retries\nasync function cobaltDownload(url, opts = {}) {\n try {\n const resp = await axios.post('https://api.cobalt.tools/api/json',\n { url, ...opts },\n { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, timeout: 25000 }\n );\n return resp.data;\n } catch (e) {\n return null;\n }\n}\n\n// ── MAIN ROUTER ───────────────────────────────────────────────────────────────\nmodule.exports = async (req, res) => {\n cors(res);\n if (req.method === 'OPTIONS') return res.status(200).end();\n\n const path = req.url.replace(/\\?.*/, '').replace(/^\\/api/, '');\n\n if (path === '' || path === '/' || path === '/status') {\n return res.json({ status: 'online', name: 'Black Node ZW API', version: '1.0.0', base_url: 'https://blacknodezw.zone.id', endpoints: 40, timestamp: new Date().toISOString() });\n }\n\n // Short URL redirect\n const shortMatch = path.match(/^\\/s\\/(.+)$/);\n if (shortMatch) {\n try {\n const db = await getDB();\n const snap = await db.collection('short_urls').doc(shortMatch[1]).get();\n if (!snap.exists) return res.status(404).send('Link not found');\n snap.ref.update({ clicks: (snap.data().clicks || 0) + 1 }).catch(() => {});\n return res.redirect(301, snap.data().url);\n } catch (e) { return res.redirect('https://blacknodezw.zone.id'); }\n }\n\n try {\n\n // ── TIKTOK ──────────────────────────────────────────────────────────────\n if (path === '/tiktok/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const decodedUrl = decodeURIComponent(url);\n try {\n const r = await axios.post('https://www.tikwm.com/api/',\n new URLSearchParams({ url: decodedUrl, hd: '1' }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 }\n );\n const d = r.data?.data;\n if (r.data?.code === 0 && d) {\n return ok(res, { title: d.title || '', author: '@' + (d.author?.unique_id || ''), author_name: d.author?.nickname || '', thumbnail: d.cover || '', download_url_hd: d.hdplay || d.play || '', download_url_sd: d.play || '', music_url: d.music || '', duration: d.duration || 0, play_count: d.play_count || 0, like_count: d.digg_count || 0 });\n }\n } catch (e) {}\n // Fallback: cobalt\n const cd = await cobaltDownload(decodedUrl);\n if (cd?.status === 'stream' || cd?.status === 'redirect') {\n return ok(res, { title: 'TikTok Video', download_url_hd: cd.url, download_url_sd: cd.url });\n }\n return err(res, 'Could not download. Make sure the video is public and the URL is correct.');\n }\n\n if (path === '/tiktok/user') {\n const { username, include_videos = 'false', video_count = '10' } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n const r = await axios.post('https://www.tikwm.com/api/user/info',\n new URLSearchParams({ unique_id: clean }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 }\n );\n if (r.data?.code !== 0) return err(res, 'User not found or account is private.');\n const u = r.data.data?.user || {}, s = r.data.data?.stats || {};\n const result = { username: u.uniqueId || clean, nickname: u.nickname || '', bio: u.signature || '', avatar: u.avatarLarger || '', verified: u.verified || false, private: u.privateAccount || false, followers: s.followerCount || 0, following: s.followingCount || 0, likes: s.heartCount || 0, video_count: s.videoCount || 0, profile_url: `https://www.tiktok.com/@${clean}` };\n if (include_videos === 'true') {\n try {\n const vr = await axios.post('https://www.tikwm.com/api/user/posts', new URLSearchParams({ unique_id: clean, count: Math.min(parseInt(video_count), 30), cursor: '0' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (vr.data?.code === 0) result.recent_videos = (vr.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0 }));\n } catch (_) {}\n }\n return ok(res, result);\n }\n\n if (path === '/tiktok/search') {\n const { q, count = '20', cursor = '0' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const r = await axios.post('https://www.tikwm.com/api/feed/search', new URLSearchParams({ keywords: q, count: Math.min(parseInt(count), 50), cursor }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 });\n if (r.data?.code !== 0) return err(res, 'Search failed. Try again.');\n return ok(res, { query: q, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' } })) });\n }\n\n if (path === '/tiktok/trending') {\n const { region = 'US', count = '20' } = req.query;\n const r = await axios.post('https://www.tikwm.com/api/feed/list', new URLSearchParams({ region, count: Math.min(parseInt(count), 50) }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 });\n if (r.data?.code !== 0) return err(res, 'Could not fetch trending.');\n return ok(res, { region, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', play_count: v.play_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' } })) });\n }\n\n // ── YOUTUBE ──────────────────────────────────────────────────────────────\n if (path === '/youtube/mp3' || path === '/youtube/mp4') {\n const { url, quality = '720' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = getYTId(url);\n if (!videoId) return err(res, 'Invalid YouTube URL. Supported: youtube.com/watch, youtu.be, youtube.com/shorts', 400);\n const fullUrl = `https://www.youtube.com/watch?v=${videoId}`;\n const isAudio = path === '/youtube/mp3';\n\n let title = 'YouTube Video', channel = 'YouTube';\n try {\n const oe = await axios.get(`https://www.youtube.com/oembed?url=${encodeURIComponent(fullUrl)}&format=json`, { timeout: 6000 });\n title = oe.data.title || title;\n channel = oe.data.author_name || channel;\n } catch (_) {}\n\n const thumbnail = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;\n\n const cd = await cobaltDownload(fullUrl, isAudio ? { isAudioOnly: true, aFormat: 'mp3', audioBitrate: '320' } : { vQuality: quality });\n if (cd?.status === 'stream' || cd?.status === 'redirect') {\n return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.url, quality: isAudio ? '320kbps' : quality + 'p', format: isAudio ? 'mp3' : 'mp4' });\n }\n if (cd?.status === 'picker' && cd.picker?.length) {\n return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.picker[0].url, quality: quality + 'p', format: 'mp4' });\n }\n\n try {\n const loaderResp = await axios.get(`https://loader.to/api/button/?url=${encodeURIComponent(fullUrl)}&f=${isAudio ? 'mp3' : 'mp4-' + quality}`, { timeout: 15000, headers: { 'User-Agent': 'Mozilla/5.0' } });\n if (loaderResp.data) {\n return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: `https://loader.to/api/download/?url=${encodeURIComponent(fullUrl)}&f=${isAudio ? 'mp3' : 'mp4'}`, format: isAudio ? 'mp3' : 'mp4', note: 'Use download_url in browser to download' });\n }\n } catch (_) {}\n\n return err(res, 'Could not fetch. cobalt.tools may be temporarily down. Try again in a moment.');\n }\n\n if (path === '/youtube/info') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = getYTId(url);\n if (!videoId) return err(res, 'Invalid YouTube URL', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (ytKey) {\n try {\n const r = await axios.get('https://www.googleapis.com/youtube/v3/videos', { params: { key: ytKey, id: videoId, part: 'snippet,statistics' }, timeout: 10000 });\n const item = r.data.items?.[0];\n if (item) return ok(res, { video_id: videoId, title: item.snippet.title, channel: item.snippet.channelTitle, thumbnail: item.snippet.thumbnails?.high?.url || '', views: parseInt(item.statistics.viewCount) || 0, likes: parseInt(item.statistics.likeCount) || 0, comments: parseInt(item.statistics.commentCount) || 0, published_at: item.snippet.publishedAt });\n } catch (_) {}\n }\n const r = await axios.get(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`, { timeout: 8000 });\n return ok(res, { video_id: videoId, title: r.data.title, channel: r.data.author_name, thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`, url: `https://www.youtube.com/watch?v=${videoId}` });\n }\n\n if (path === '/youtube/channel') {\n const { channel } = req.query;\n if (!channel) return err(res, 'Missing: channel', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (!ytKey) return err(res, 'Set YOUTUBE_API_KEY in Vercel environment variables for this endpoint.', 400);\n const search = await axios.get('https://www.googleapis.com/youtube/v3/search', { params: { key: ytKey, q: channel.replace(/^@/, ''), type: 'channel', part: 'id', maxResults: 1 }, timeout: 10000 });\n const channelId = search.data.items?.[0]?.id?.channelId;\n if (!channelId) return err(res, 'Channel not found.', 404);\n const stats = await axios.get('https://www.googleapis.com/youtube/v3/channels', { params: { key: ytKey, id: channelId, part: 'snippet,statistics' }, timeout: 10000 });\n const ch = stats.data.items?.[0];\n if (!ch) return err(res, 'Channel not found.', 404);\n return ok(res, { channel_id: channelId, title: ch.snippet.title, description: ch.snippet.description?.slice(0, 300), avatar: ch.snippet.thumbnails?.high?.url || '', subscribers: parseInt(ch.statistics.subscriberCount) || 0, total_views: parseInt(ch.statistics.viewCount) || 0, video_count: parseInt(ch.statistics.videoCount) || 0 });\n }\n\n // ── INSTAGRAM ────────────────────────────────────────────────────────────\n if (path === '/instagram/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const decodedUrl = decodeURIComponent(url);\n const cd = await cobaltDownload(decodedUrl);\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { type: 'video', download_url: cd.url });\n if (cd?.status === 'picker') return ok(res, { type: 'gallery', items: cd.picker.map(p => ({ type: p.type, url: p.url })) });\n try {\n const snapResp = await axios.post('https://snapinsta.app/action_download.php',\n new URLSearchParams({ url: decodedUrl }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0', Referer: 'https://snapinsta.app/', Origin: 'https://snapinsta.app' }, timeout: 20000 }\n );\n const html = typeof snapResp.data === 'string' ? snapResp.data : '';\n const videoMatch = html.match(/href=\"(https:\\/\\/[^\"]*\\.mp4[^\"]*)\"/);\n const imgMatch = html.match(/src=\"(https:\\/\\/[^\"]*\\.jpg[^\"]*)\"/);\n if (videoMatch) return ok(res, { type: 'video', download_url: videoMatch[1] });\n if (imgMatch) return ok(res, { type: 'image', download_url: imgMatch[1] });\n } catch (_) {}\n try {\n const r = await axios.post('https://v3.saveinsta.app/api/ajaxSearch',\n new URLSearchParams({ q: decodedUrl, t: 'media', lang: 'en' }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 }\n );\n if (r.data?.data) {\n const d = r.data.data;\n if (d.medias?.length) return ok(res, { type: d.type || 'video', items: d.medias.map(m => ({ url: m.url, quality: m.quality || 'hd', extension: m.extension || 'mp4' })), download_url: d.medias[0]?.url });\n }\n } catch (_) {}\n return err(res, 'Could not download Instagram media. The post may be private or Instagram is blocking scrapers. Try again.');\n }\n\n if (path === '/instagram/user') {\n const { username } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n try {\n const r = await axios.get(`https://www.instagram.com/api/v1/users/web_profile_info/?username=${clean}`, {\n headers: { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15', 'X-IG-App-ID': '936619743392459', Referer: `https://www.instagram.com/${clean}/`, Accept: '*/*' },\n timeout: 15000\n });\n const u = r.data?.data?.user;\n if (u) return ok(res, { username: u.username, full_name: u.full_name || '', bio: u.biography || '', avatar: u.profile_pic_url_hd || u.profile_pic_url || '', verified: u.is_verified || false, private: u.is_private || false, followers: u.edge_followed_by?.count || 0, following: u.edge_follow?.count || 0, posts: u.edge_owner_to_timeline_media?.count || 0 });\n } catch (_) {}\n try {\n const r = await axios.get(`https://www.instagram.com/${clean}/`, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', Accept: 'text/html' }, timeout: 12000 });\n const desc = r.data.match(/ { if (!s) return 0; s = s.replace(/,/g, '').toUpperCase(); if (s.includes('M')) return Math.round(parseFloat(s) * 1e6); if (s.includes('K')) return Math.round(parseFloat(s) * 1e3); return parseInt(s) || 0; };\n return ok(res, { username: clean, followers: fMatch ? parseK(fMatch[1]) : 0, following: fgMatch ? parseK(fgMatch[1]) : 0, posts: pMatch ? parseK(pMatch[1]) : 0, profile_url: `https://www.instagram.com/${clean}/`, note: 'Instagram limits profile data. Set RAPIDAPI_KEY for full data.' });\n } catch (e) {\n return err(res, 'Instagram profile fetch failed. Instagram actively blocks scrapers. Try again later.');\n }\n }\n\n // ── OTHER DOWNLOADERS ────────────────────────────────────────────────────\n if (path === '/facebook/download') {\n const { url, quality = 'hd' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const decodedUrl = decodeURIComponent(url);\n const cd = await cobaltDownload(decodedUrl);\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { download_url: cd.url, quality });\n if (cd?.status === 'picker' && cd.picker?.length) {\n const pick = quality === 'hd' ? cd.picker[0] : cd.picker[cd.picker.length - 1];\n return ok(res, { download_url: pick.url, quality, all_qualities: cd.picker.map(p => ({ url: p.url, quality: p.quality || 'unknown' })) });\n }\n try {\n const r = await axios.post('https://www.getfvid.com/downloader',\n new URLSearchParams({ url: decodedUrl }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0', Referer: 'https://www.getfvid.com/', Origin: 'https://www.getfvid.com' }, timeout: 20000 }\n );\n const hdMatch = r.data.match(/href=\"(https:\\/\\/[^\"]+\\.mp4[^\"]*)\"\\s[^>]*>HD/i);\n const sdMatch = r.data.match(/href=\"(https:\\/\\/[^\"]+\\.mp4[^\"]*)\"\\s[^>]*>SD/i);\n const anyMatch = r.data.match(/href=\"(https:\\/\\/video[^\\.]*\\.facebook[^\"]+)\"/);\n const dlUrl = (quality === 'hd' ? hdMatch?.[1] : sdMatch?.[1]) || hdMatch?.[1] || sdMatch?.[1] || anyMatch?.[1];\n if (dlUrl) return ok(res, { download_url: dlUrl, quality: hdMatch ? 'hd' : 'sd' });\n } catch (_) {}\n try {\n const r = await axios.post('https://fdown.net/download.php',\n new URLSearchParams({ URLz: decodedUrl }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0', Referer: 'https://fdown.net/' }, timeout: 20000 }\n );\n const hdMatch = r.data.match(/id=\"hdlink\"\\s+href=\"([^\"]+)\"/);\n const sdMatch = r.data.match(/id=\"sdlink\"\\s+href=\"([^\"]+)\"/);\n if (hdMatch || sdMatch) return ok(res, { download_url_hd: hdMatch?.[1] || '', download_url_sd: sdMatch?.[1] || '', download_url: hdMatch?.[1] || sdMatch?.[1] });\n } catch (_) {}\n return err(res, 'Could not download Facebook video. Make sure the video is public.');\n }\n\n if (path === '/twitter/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const decodedUrl = decodeURIComponent(url);\n const cd = await cobaltDownload(decodedUrl);\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { download_url: cd.url, quality: 'hd' });\n if (cd?.status === 'picker' && cd.picker?.length) return ok(res, { items: cd.picker.map(p => ({ quality: p.quality || 'unknown', url: p.url })) });\n try {\n const r = await axios.get(`https://twitsave.com/info?url=${encodeURIComponent(decodedUrl)}`, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n const match = r.data.match(/downloadFile\\(\"(https:\\/\\/[^\"]+\\.mp4[^\"]*)\"/);\n if (match) return ok(res, { download_url: match[1] });\n } catch (_) {}\n return err(res, 'Could not download. Make sure the tweet has a video and is public.');\n }\n\n if (path === '/pinterest/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n try {\n const cheerio = require('cheerio');\n const r = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0', Accept: 'text/html' }, maxRedirects: 5, timeout: 15000 });\n const $ = cheerio.load(r.data);\n const videoUrl = $('meta[property=\"og:video\"]').attr('content') || '';\n const imageUrl = $('meta[property=\"og:image\"]').attr('content') || '';\n if (!videoUrl && !imageUrl) return err(res, 'No media found. Make sure the pin is public.');\n const hdImage = imageUrl.replace(/\\/\\d+x\\//, '/originals/');\n return ok(res, { type: videoUrl ? 'video' : 'image', title: $('meta[property=\"og:title\"]').attr('content') || '', download_url: videoUrl || hdImage, thumbnail: imageUrl, hd_image: hdImage });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── IMAGE ────────────────────────────────────────────────────────────────\n if (path === '/image/search') {\n const { q, count = '10' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const gKey = process.env.GOOGLE_SEARCH_API_KEY, cx = process.env.GOOGLE_SEARCH_CX;\n if (gKey && cx) {\n try {\n const r = await axios.get('https://www.googleapis.com/customsearch/v1', { params: { key: gKey, cx, q, searchType: 'image', num: Math.min(parseInt(count), 10) }, timeout: 10000 });\n return ok(res, { query: q, images: (r.data.items || []).map(i => ({ url: i.link, thumbnail: i.image?.thumbnailLink || '', title: i.title, source: i.image?.contextLink || '' })) });\n } catch (_) {}\n }\n try {\n const r = await axios.get(`https://source.unsplash.com/featured/?${encodeURIComponent(q)}`, { maxRedirects: 0, timeout: 8000, validateStatus: s => s < 400 });\n const finalUrl = r.headers?.location || r.request?.res?.responseUrl || '';\n if (finalUrl) return ok(res, { query: q, images: [{ url: finalUrl, thumbnail: finalUrl, title: q, source: 'unsplash.com' }], note: 'Set GOOGLE_SEARCH_API_KEY for more results' });\n } catch (_) {}\n try {\n const r = await axios.get('https://pixabay.com/api/', { params: { key: process.env.PIXABAY_API_KEY || '44556677-abc123def456', q, image_type: 'photo', per_page: Math.min(parseInt(count), 20) }, timeout: 10000 });\n if (r.data?.hits?.length) return ok(res, { query: q, images: r.data.hits.map(i => ({ url: i.largeImageURL || i.webformatURL, thumbnail: i.previewURL, title: i.tags, source: 'pixabay.com' })) });\n } catch (_) {}\n return err(res, 'Image search failed. Set GOOGLE_SEARCH_API_KEY and GOOGLE_SEARCH_CX in Vercel environment variables for reliable image search.');\n }\n\n if (path === '/image/generate') {\n const prompt = req.body?.prompt || req.query?.prompt;\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const style = req.body?.style || req.query?.style || 'realistic';\n const styleMap = { anime: 'anime style,', art: 'digital art,', cartoon: 'cartoon,', sketch: 'pencil sketch,' };\n const fullPrompt = ((styleMap[style] || '') + ' ' + prompt).trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/images/generations', { model: 'dall-e-3', prompt: fullPrompt, n: 1, size: '1024x1024' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 60000 });\n return ok(res, { prompt: fullPrompt, image_url: r.data.data[0]?.url, model: 'dall-e-3' });\n } catch (_) {}\n }\n const seed = Math.floor(Math.random() * 1000000);\n const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(fullPrompt)}?width=1024&height=1024&model=flux&seed=${seed}&nologo=true`;\n return ok(res, { prompt: fullPrompt, image_url: imageUrl, model: 'flux', seed, note: 'Open image_url in browser to view the generated image' });\n }\n\n if (path === '/image/removebg') {\n const imageUrl = req.body?.url || req.query?.url;\n if (!imageUrl) return err(res, 'Missing: url', 400);\n const rbKey = process.env.REMOVEBG_API_KEY;\n if (rbKey) {\n try {\n const FormData = require('form-data');\n const form = new FormData();\n form.append('image_url', imageUrl);\n form.append('size', 'auto');\n const r = await axios.post('https://api.remove.bg/v1.0/removebg', form, { headers: { ...form.getHeaders(), 'X-Api-Key': rbKey }, responseType: 'arraybuffer', timeout: 30000 });\n const b64 = Buffer.from(r.data).toString('base64');\n return ok(res, { original: imageUrl, result_base64: `data:image/png;base64,${b64}`, format: 'png', provider: 'remove.bg' });\n } catch (_) {}\n }\n try {\n const r = await axios.post('https://api.photoroom.com/v1/segment', { image_url: imageUrl }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });\n if (r.data?.image_url) return ok(res, { original: imageUrl, result_url: r.data.image_url, format: 'png', provider: 'photoroom' });\n } catch (_) {}\n return err(res, 'Background removal requires REMOVEBG_API_KEY. Get a free key at remove.bg/api');\n }\n\n // ── AI ───────────────────────────────────────────────────────────────────\n if (path === '/ai/chat') {\n const { message, system = 'You are a helpful AI assistant.', model = 'default' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 25000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'gpt-3.5-turbo', tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (_) {}\n }\n const groqKey = process.env.GROQ_API_KEY;\n if (groqKey) {\n try {\n const r = await axios.post('https://api.groq.com/openai/v1/chat/completions', { model: 'llama3-8b-8192', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Authorization': `Bearer ${groqKey}`, 'Content-Type': 'application/json' }, timeout: 20000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'llama3-8b (groq)', tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (_) {}\n }\n try {\n const r = await axios.post('https://api.deepinfra.com/v1/openai/chat/completions', { model: 'meta-llama/Meta-Llama-3-8B-Instruct', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'llama-3-8b' });\n } catch (e) { return err(res, 'AI service temporarily unavailable. Set OPENAI_API_KEY or GROQ_API_KEY (free at groq.com) for reliable AI chat.'); }\n }\n\n if (path === '/ai/generate') {\n const { prompt, type = 'default', length = 'medium', tone = 'professional' } = req.body || {};\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const typeMap = { article: 'Write a detailed article about:', code: 'Write clean well-commented code for:', story: 'Write a creative short story about:', email: 'Write a professional email for:', essay: 'Write an essay about:', caption: 'Write social media captions for:' };\n const maxTokens = { short: 300, medium: 700, long: 1400 }[length] || 700;\n const fullPrompt = `${typeMap[type] || ''} ${prompt}. Tone: ${tone}`.trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: fullPrompt }], max_tokens: maxTokens }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 });\n const output = r.data.choices[0]?.message?.content || '';\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length });\n } catch (_) {}\n }\n try {\n const r = await axios.get(`https://text.pollinations.ai/${encodeURIComponent(fullPrompt)}`, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 30000 });\n const output = typeof r.data === 'string' ? r.data : JSON.stringify(r.data);\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length, model: 'pollinations' });\n } catch (e) { return err(res, 'AI generation timed out. Set OPENAI_API_KEY or GROQ_API_KEY for faster responses.'); }\n }\n\n if (path === '/ai/tts') {\n const text = req.body?.text || req.query?.text;\n const voice = req.body?.voice || req.query?.voice || 'Brian';\n if (!text) return err(res, 'Missing: text', 400);\n if (text.length > 3000) return err(res, 'Text too long. Max 3000 characters.', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/audio/speech', { model: 'tts-1', input: text, voice: 'alloy' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, responseType: 'arraybuffer', timeout: 25000 });\n res.setHeader('Content-Type', 'audio/mpeg');\n return res.send(Buffer.from(r.data));\n } catch (_) {}\n }\n const voices = ['Brian','Amy','Emma','Matthew','Joanna','Joey','Kendra','Kimberly','Salli','Justin'];\n const sel = voices.includes(voice) ? voice : 'Brian';\n const audioUrl = `https://api.streamelements.com/kappa/v2/speech?voice=${sel}&text=${encodeURIComponent(text)}`;\n return ok(res, { text, voice: sel, audio_url: audioUrl, format: 'mp3', note: 'Open audio_url in browser or use in an audio player' });\n }\n\n if (path === '/ai/translate') {\n const { text, to, from = 'auto' } = req.query;\n if (!text) return err(res, 'Missing: text', 400);\n if (!to) return err(res, 'Missing: to (target language code e.g. es, fr, sn)', 400);\n try {\n const r = await axios.get('https://translate.googleapis.com/translate_a/single', { params: { client: 'gtx', sl: from === 'auto' ? 'auto' : from, tl: to, dt: 't', q: text }, headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data[0]?.map(s => s[0]).join('') || '', from: r.data[2] || from, to });\n } catch (_) {\n try {\n const r = await axios.get('https://api.mymemory.translated.net/get', { params: { q: text, langpair: `${from}|${to}` }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data.responseData.translatedText, from, to });\n } catch (e) { return err(res, 'Translation failed. Try again.'); }\n }\n }\n\n if (path === '/ai/music') {\n const prompt = req.body?.prompt || req.query?.prompt;\n const style = req.body?.style || req.query?.style || 'pop';\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const hfKey = process.env.HUGGINGFACE_API_KEY;\n if (hfKey) {\n try {\n const r = await axios.post('https://api-inference.huggingface.co/models/facebook/musicgen-small',\n { inputs: `${style} music: ${prompt}` },\n { headers: { 'Authorization': `Bearer ${hfKey}`, 'Content-Type': 'application/json' }, responseType: 'arraybuffer', timeout: 60000 }\n );\n const b64 = Buffer.from(r.data).toString('base64');\n return ok(res, { prompt, style, audio_base64: `data:audio/wav;base64,${b64}`, format: 'wav', model: 'musicgen-small', note: 'Set HUGGINGFACE_API_KEY for this endpoint' });\n } catch (_) {}\n }\n return ok(res, { prompt, style, note: 'Set HUGGINGFACE_API_KEY for AI music generation. Free at huggingface.co', alternative: 'Try suno.com or udio.com for free AI music generation' });\n }\n\n // ── WEATHER ──────────────────────────────────────────────────────────────\n if (path === '/weather') {\n const { city, units = 'metric' } = req.query;\n if (!city) return err(res, 'Missing: city', 400);\n try {\n const geo = await axios.get('https://nominatim.openstreetmap.org/search', { params: { q: city, format: 'json', limit: 1 }, headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 10000 });\n if (!geo.data?.length) return err(res, `City not found: \"${city}\"`, 404);\n const { lat, lon, display_name } = geo.data[0];\n const w = await axios.get('https://api.open-meteo.com/v1/forecast', { params: { latitude: lat, longitude: lon, current: 'temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,precipitation', daily: 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum', temperature_unit: units === 'imperial' ? 'fahrenheit' : 'celsius', wind_speed_unit: units === 'imperial' ? 'mph' : 'kmh', timezone: 'auto', forecast_days: 7 }, timeout: 12000 });\n const c = w.data.current, unit = units === 'imperial' ? '°F' : '°C';\n const WMO = { 0:'Clear sky',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Foggy',51:'Light drizzle',61:'Slight rain',63:'Moderate rain',65:'Heavy rain',71:'Slight snow',80:'Showers',95:'Thunderstorm' };\n return ok(res, { city: display_name.split(',')[0], full_location: display_name, coordinates: { lat: parseFloat(lat), lon: parseFloat(lon) }, temperature: `${c.temperature_2m}${unit}`, feels_like: `${c.apparent_temperature}${unit}`, condition: WMO[c.weather_code] || 'Unknown', humidity: `${c.relative_humidity_2m}%`, wind_speed: `${c.wind_speed_10m} ${units === 'imperial' ? 'mph' : 'km/h'}`, precipitation: `${c.precipitation}mm`, units, forecast: w.data.daily.time.map((d, i) => ({ date: d, condition: WMO[w.data.daily.weather_code[i]] || 'Unknown', high: `${w.data.daily.temperature_2m_max[i]}${unit}`, low: `${w.data.daily.temperature_2m_min[i]}${unit}`, precipitation: `${w.data.daily.precipitation_sum[i]}mm` })) });\n } catch (e) { return err(res, `Weather fetch failed: ${e.message}`); }\n }\n\n // ── CURRENCY ─────────────────────────────────────────────────────────────\n if (path === '/currency/convert') {\n let { from = 'USD', to, amount = '1', action } = req.query;\n from = from.replace(/[^A-Za-z]/g, '').toUpperCase().slice(0, 3);\n if (!from || from.length < 2) from = 'USD';\n try {\n const r = await axios.get(`https://open.er-api.com/v6/latest/${from}`, { timeout: 10000 });\n if (r.data.result !== 'success') throw new Error('Exchange rate API failed');\n if (action === 'rates') return ok(res, { base: from, rates: r.data.rates, last_updated: r.data.time_last_update_utc });\n if (!to) return err(res, 'Missing: to (target currency code e.g. ZAR, EUR, GBP)', 400);\n to = to.replace(/[^A-Za-z]/g, '').toUpperCase().slice(0, 3);\n const rate = r.data.rates[to];\n if (!rate) return err(res, `Currency not found: \"${to}\". Use standard codes like USD, EUR, ZAR, GBP.`, 400);\n const amt = parseFloat(amount) || 1;\n return ok(res, { from, to, amount: amt, rate, result: parseFloat((amt * rate).toFixed(6)), formatted: `${(amt * rate).toFixed(2)} ${to}`, last_updated: r.data.time_last_update_utc });\n } catch (e) {\n try {\n const to2 = (to || 'USD').replace(/[^A-Za-z]/g, '').toUpperCase().slice(0, 3);\n const r = await axios.get(`https://api.frankfurter.app/latest?from=${from}&to=${to2}`, { timeout: 10000 });\n const rate = r.data.rates[to2];\n if (!rate) return err(res, `Currency not found: \"${to2}\"`, 400);\n const amt = parseFloat(amount) || 1;\n return ok(res, { from, to: to2, amount: amt, rate, result: parseFloat((amt * rate).toFixed(6)), formatted: `${(amt * rate).toFixed(2)} ${to2}` });\n } catch (e2) { return err(res, 'Currency conversion failed. Try again.'); }\n }\n }\n\n // ── QR CODE ──────────────────────────────────────────────────────────────\n if (path === '/qrcode/generate') {\n const { data, size = '300', color = '000000', bg = 'ffffff', format = 'json' } = req.query;\n if (!data) return err(res, 'Missing: data', 400);\n const sizeNum = Math.min(Math.max(parseInt(size) || 300, 100), 1000);\n const opts = { width: sizeNum, margin: 2, color: { dark: `#${color.replace('#', '')}`, light: `#${bg.replace('#', '')}` } };\n if (format === 'svg') {\n const svg = await QRCode.toString(data, { ...opts, type: 'svg' });\n res.setHeader('Content-Type', 'image/svg+xml');\n return res.send(svg);\n }\n if (format === 'png') {\n res.setHeader('Content-Type', 'image/png');\n return res.send(await QRCode.toBuffer(data, opts));\n }\n const dataUrl = await QRCode.toDataURL(data, opts);\n return ok(res, { data, format: 'base64', size: `${sizeNum}x${sizeNum}`, image: dataUrl, note: 'Use format=png or format=svg in URL for direct image response' });\n }\n\n // ── IP LOOKUP ────────────────────────────────────────────────────────────\n if (path === '/ip/lookup') {\n let { ip } = req.query;\n if (!ip) ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || '';\n try {\n const r = await axios.get(`https://ipwho.is/${ip}`, { timeout: 8000 });\n if (r.data.success) return ok(res, { ip: r.data.ip, country: r.data.country, country_code: r.data.country_code, region: r.data.region, city: r.data.city, latitude: r.data.latitude, longitude: r.data.longitude, timezone: r.data.timezone?.id || '', isp: r.data.connection?.isp || '', org: r.data.connection?.org || '' });\n } catch (_) {}\n try {\n const r = await axios.get(`http://ip-api.com/json/${ip}`, { timeout: 8000 });\n if (r.data.status === 'success') return ok(res, { ip: r.data.query, country: r.data.country, country_code: r.data.countryCode, region: r.data.regionName, city: r.data.city, latitude: r.data.lat, longitude: r.data.lon, timezone: r.data.timezone, isp: r.data.isp });\n } catch (_) {}\n return err(res, 'IP lookup failed. Try again.');\n }\n\n // ── URL SHORTENER ────────────────────────────────────────────────────────\n if (path === '/url/shorten') {\n const url = req.body?.url || req.query?.url;\n const alias = req.body?.alias || req.query?.alias || Math.random().toString(36).slice(2, 8);\n if (!url) return err(res, 'Missing: url', 400);\n try { new URL(url); } catch { return err(res, 'Invalid URL format. Must start with https://', 400); }\n try {\n const db = await getDB();\n const ref = db.collection('short_urls').doc(alias);\n const snap = await ref.get();\n if (!snap.exists) await ref.set({ url, alias, clicks: 0, created_at: new Date() });\n return ok(res, { original: url, short: `https://blacknodezw.zone.id/api/s/${alias}`, alias, clicks: snap.exists ? snap.data().clicks : 0 });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── NEWS ─────────────────────────────────────────────────────────────────\n if (path === '/news') {\n const { q, category = 'general', limit = '10' } = req.query;\n const ndKey = process.env.NEWSDATA_API_KEY;\n if (ndKey) {\n try {\n const params = { apikey: ndKey, language: 'en', size: Math.min(parseInt(limit), 50) };\n if (q) params.q = q; else params.category = category;\n const r = await axios.get('https://newsdata.io/api/1/news', { params, timeout: 12000 });\n if (r.data.status === 'success') return ok(res, { query: q || category, articles: (r.data.results || []).map(a => ({ title: a.title || '', description: a.description || '', url: a.link || '', image: a.image_url || '', source: a.source_id || '', published_at: a.pubDate || '' })) });\n } catch (_) {}\n }\n const RSS = { general: 'https://feeds.bbci.co.uk/news/rss.xml', technology: 'https://feeds.bbci.co.uk/news/technology/rss.xml', sports: 'https://feeds.bbci.co.uk/sport/rss.xml', business: 'https://feeds.bbci.co.uk/news/business/rss.xml', health: 'https://feeds.bbci.co.uk/news/health/rss.xml', science: 'https://feeds.bbci.co.uk/news/science_and_environment/rss.xml' };\n try {\n const r = await axios.get(RSS[category] || RSS.general, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 12000 });\n const items = [];\n for (const m of r.data.matchAll(/([\\s\\S]*?)<\\/item>/g)) {\n const i = m[1];\n const title = (i.match(/<!\\[CDATA\\[(.+?)\\]\\]>/) || i.match(/<title>(.+?)<\\/title>/))?.[1] || '';\n const link = i.match(/<link>(.+?)<\\/link>/)?.[1] || '';\n const desc = (i.match(/<description><!\\[CDATA\\[(.+?)\\]\\]>/) || i.match(/<description>(.+?)<\\/description>/))?.[1] || '';\n const pubDate = i.match(/<pubDate>(.+?)<\\/pubDate>/)?.[1] || '';\n if (title) items.push({ title: title.trim(), description: desc.replace(/<[^>]+>/g, '').slice(0, 200), url: link.trim(), source: 'BBC News', published_at: pubDate });\n if (items.length >= parseInt(limit)) break;\n }\n return ok(res, { query: q || category, total: items.length, articles: items, note: 'Set NEWSDATA_API_KEY for 200+ news sources' });\n } catch (e) { return err(res, 'News fetch failed. Try again.'); }\n }\n\n // ── DICTIONARY ───────────────────────────────────────────────────────────\n if (path === '/dictionary') {\n const { word, lang = 'en' } = req.query;\n if (!word) return err(res, 'Missing: word', 400);\n try {\n const r = await axios.get(`https://api.dictionaryapi.dev/api/v2/entries/${lang}/${encodeURIComponent(word.trim().toLowerCase())}`, { timeout: 10000 });\n const entry = r.data[0];\n return ok(res, { word: entry.word, phonetic: entry.phonetic || '', phonetics: (entry.phonetics || []).filter(p => p.text || p.audio).map(p => ({ text: p.text || '', audio: p.audio || '' })), meanings: (entry.meanings || []).map(m => ({ part_of_speech: m.partOfSpeech, definitions: (m.definitions || []).slice(0, 3).map(d => ({ definition: d.definition, example: d.example || '' })), synonyms: (m.synonyms || []).slice(0, 8), antonyms: (m.antonyms || []).slice(0, 5) })) });\n } catch (e) {\n if (e.response?.status === 404) return err(res, `No definition found for: \"${word}\"`, 404);\n return err(res, e.message);\n }\n }\n\n // ── GITHUB ───────────────────────────────────────────────────────────────\n if (path === '/github/user') {\n const { username, action = 'user', repo } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const headers = process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 'User-Agent': 'BlackNodeZW/1.0' } : { 'User-Agent': 'BlackNodeZW/1.0' };\n try {\n if (action === 'repos') {\n const r = await axios.get(`https://api.github.com/users/${username}/repos?sort=stars&per_page=20`, { headers, timeout: 10000 });\n return ok(res, { username, repos: r.data.map(r => ({ name: r.name, description: r.description || '', url: r.html_url, language: r.language || '', stars: r.stargazers_count, forks: r.forks_count })) });\n }\n if (action === 'repo' && repo) {\n const r = await axios.get(`https://api.github.com/repos/${username}/${repo}`, { headers, timeout: 10000 });\n return ok(res, { name: r.data.name, description: r.data.description || '', url: r.data.html_url, language: r.data.language || '', stars: r.data.stargazers_count, forks: r.data.forks_count, issues: r.data.open_issues_count });\n }\n const r = await axios.get(`https://api.github.com/users/${username}`, { headers, timeout: 10000 });\n const u = r.data;\n return ok(res, { username: u.login, name: u.name || '', bio: u.bio || '', avatar: u.avatar_url, url: u.html_url, followers: u.followers, following: u.following, public_repos: u.public_repos, location: u.location || '', company: u.company || '', twitter: u.twitter_username || '' });\n } catch (e) { return err(res, e.response?.data?.message || e.message); }\n }\n\n // ── SCREENSHOT ───────────────────────────────────────────────────────────\n if (path === '/screenshot') {\n const { url, width = '1280', height = '720', format = 'jpg' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n try { new URL(url); } catch { return err(res, 'Invalid URL. Must start with https://', 400); }\n const w = Math.min(parseInt(width) || 1280, 1920);\n const h = Math.min(parseInt(height) || 720, 1080);\n const ssKey = process.env.SCREENSHOTONE_API_KEY;\n if (ssKey) {\n try {\n const r = await axios.get('https://api.screenshotone.com/take', { params: { access_key: ssKey, url, viewport_width: w, viewport_height: h, format: 'jpg', block_ads: true }, responseType: 'arraybuffer', timeout: 30000 });\n res.setHeader('Content-Type', 'image/jpeg');\n return res.send(Buffer.from(r.data));\n } catch (_) {}\n }\n try {\n const r = await axios.get(`https://image.thum.io/get/width/${w}/crop/${h}/${encodeURIComponent(url)}`, { responseType: 'arraybuffer', timeout: 25000, headers: { 'User-Agent': 'BlackNodeZW/1.0' } });\n if (r.data?.byteLength > 1000) {\n res.setHeader('Content-Type', 'image/jpeg');\n return res.send(Buffer.from(r.data));\n }\n } catch (_) {}\n return ok(res, { url, screenshot_url: `https://image.thum.io/get/width/${w}/crop/${h}/${encodeURIComponent(url)}`, note: 'Open screenshot_url in browser to view the screenshot' });\n }\n\n // ── FUN ───────────────────────────────────────────────────────────────────\n if (path === '/fun') {\n const { type = 'quote', category = 'general' } = req.query;\n try {\n if (type === 'quote') { const r = await axios.get('https://zenquotes.io/api/random', { timeout: 8000 }); return ok(res, { type: 'quote', content: r.data[0].q, author: r.data[0].a }); }\n if (type === 'joke') { const r = await axios.get('https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,racist,sexist', { timeout: 8000 }); return ok(res, { type: 'joke', category: r.data.category, joke: r.data.joke || null, setup: r.data.setup || null, delivery: r.data.delivery || null }); }\n if (type === 'fact') { const r = await axios.get('https://uselessfacts.jsph.pl/api/v2/facts/random?language=en', { timeout: 8000 }); return ok(res, { type: 'fact', content: r.data.text }); }\n if (type === 'trivia') { const r = await axios.get('https://opentdb.com/api.php?amount=1&type=multiple', { timeout: 8000 }); const q = r.data.results?.[0]; if (q) return ok(res, { type: 'trivia', category: q.category, difficulty: q.difficulty, question: q.question, correct_answer: q.correct_answer, incorrect_answers: q.incorrect_answers }); }\n if (type === 'riddle') { const r = await axios.get('https://riddles-api.vercel.app/random', { timeout: 8000 }); return ok(res, { type: 'riddle', riddle: r.data.riddle, answer: r.data.answer }); }\n return err(res, `Invalid type: ${type}. Use: quote, joke, fact, trivia, riddle`, 400);\n } catch (e) { return err(res, e.message); }\n }\n\n // ── SPOTIFY ──────────────────────────────────────────────────────────────\n if (path === '/spotify/track') {\n const { url, action, q } = req.query;\n const clientId = process.env.SPOTIFY_CLIENT_ID, clientSecret = process.env.SPOTIFY_CLIENT_SECRET;\n if (!clientId || !clientSecret) return err(res, 'Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in Vercel environment variables. Free at developer.spotify.com', 400);\n try {\n const tokenR = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({ grant_type: 'client_credentials' }), { headers: { 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 });\n const headers = { 'Authorization': `Bearer ${tokenR.data.access_token}` };\n if (action === 'search' && q) {\n const r = await axios.get('https://api.spotify.com/v1/search', { params: { q, type: 'track', limit: 10, market: 'US' }, headers, timeout: 10000 });\n return ok(res, { query: q, tracks: (r.data.tracks?.items || []).map(t => ({ id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', preview_url: t.preview_url || '', spotify_url: t.external_urls?.spotify || '' })) });\n }\n if (!url) return err(res, 'Missing: url or use action=search&q=query', 400);\n const trackId = url.match(/track\\/([a-zA-Z0-9]+)/)?.[1];\n if (!trackId) return err(res, 'Invalid Spotify URL', 400);\n const r = await axios.get(`https://api.spotify.com/v1/tracks/${trackId}`, { headers, timeout: 10000 });\n const t = r.data;\n return ok(res, { id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', duration_ms: t.duration_ms, preview_url: t.preview_url || '', popularity: t.popularity, spotify_url: t.external_urls?.spotify || '' });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── MOVIE APIs ─────────────────────────────────────────────────────\n if (path === '/movie/search') {\n const { q, year, type = 'movie', page = '1' } = req.query;\n if (!q) return err(res, 'Missing: q (movie title to search)', 400);\n const omdbKey = process.env.OMDB_API_KEY || 'trilogy';\n try {\n const params = { apikey: omdbKey, s: q, type, page };\n if (year) params.y = year;\n const r = await axios.get('https://www.omdbapi.com/', { params, timeout: 10000 });\n if (r.data.Response === 'False') return err(res, r.data.Error || 'No results found.', 404);\n return ok(res, { query: q, total: parseInt(r.data.totalResults) || 0, page: parseInt(page), results: (r.data.Search || []).map(m => ({ imdb_id: m.imdbID, title: m.Title, year: m.Year, type: m.Type, poster: m.Poster !== 'N/A' ? m.Poster : null })) });\n } catch (e) { return err(res, 'Movie search failed. Set OMDB_API_KEY (free at omdbapi.com) for reliable results.'); }\n }\n\n if (path === '/movie/details') {\n const { id, title } = req.query;\n if (!id && !title) return err(res, 'Missing: id (IMDb ID like tt1234567) or title', 400);\n const omdbKey = process.env.OMDB_API_KEY || 'trilogy';\n try {\n const params = { apikey: omdbKey, plot: 'full' };\n if (id) params.i = id; else params.t = title;\n const r = await axios.get('https://www.omdbapi.com/', { params, timeout: 10000 });\n if (r.data.Response === 'False') return err(res, r.data.Error || 'Movie not found.', 404);\n const m = r.data;\n return ok(res, { imdb_id: m.imdbID, title: m.Title, year: m.Year, rated: m.Rated, released: m.Released, runtime: m.Runtime, genre: m.Genre, director: m.Director, actors: m.Actors, plot: m.Plot, language: m.Language, country: m.Country, awards: m.Awards, poster: m.Poster !== 'N/A' ? m.Poster : null, imdb_rating: m.imdbRating, imdb_votes: m.imdbVotes, type: m.Type, box_office: m.BoxOffice || null });\n } catch (e) { return err(res, 'Movie details fetch failed. Set OMDB_API_KEY (free at omdbapi.com).'); }\n }\n\n if (path === '/movie/trending') {\n const tmdbKey = process.env.TMDB_API_KEY;\n if (!tmdbKey) return err(res, 'Set TMDB_API_KEY in Vercel environment variables. Free at themoviedb.org/settings/api', 400);\n const { time_window = 'week' } = req.query;\n try {\n const r = await axios.get(`https://api.themoviedb.org/3/trending/movie/${time_window}`, { params: { api_key: tmdbKey }, timeout: 10000 });\n return ok(res, { time_window, results: (r.data.results || []).map(m => ({ id: m.id, title: m.title, overview: m.overview?.slice(0, 200), release_date: m.release_date, rating: m.vote_average, poster: m.poster_path ? `https://image.tmdb.org/t/p/w500${m.poster_path}` : null, popularity: m.popularity })) });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── WHATSAPP BOT API ───────────────────────────────────────────────\n if (path === '/whatsapp/send') {\n const { phone, message } = req.body || {};\n if (!phone || !message) return err(res, 'Missing: phone (with country code) and message', 400);\n const waKey = process.env.WHATSAPP_API_KEY;\n const waPhone = process.env.WHATSAPP_PHONE_ID;\n if (!waKey || !waPhone) return err(res, 'Set WHATSAPP_API_KEY and WHATSAPP_PHONE_ID in Vercel environment variables. Get from developers.facebook.com/docs/whatsapp', 400);\n try {\n const r = await axios.post(`https://graph.facebook.com/v18.0/${waPhone}/messages`, { messaging_product: 'whatsapp', to: phone.replace(/\\D/g, ''), type: 'text', text: { body: message } }, { headers: { 'Authorization': `Bearer ${waKey}`, 'Content-Type': 'application/json' }, timeout: 15000 });\n return ok(res, { success: true, message_id: r.data.messages?.[0]?.id, to: phone, status: 'sent' });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n if (path === '/whatsapp/verify') {\n const mode = req.query['hub.mode'];\n const token = req.query['hub.verify_token'];\n const challenge = req.query['hub.challenge'];\n const verifyToken = process.env.WHATSAPP_VERIFY_TOKEN || 'blacknodezw';\n if (mode === 'subscribe' && token === verifyToken) return res.status(200).send(challenge);\n return err(res, 'Verification failed', 403);\n }\n\n // ── TELEGRAM BOT API ───────────────────────────────────────────────\n if (path === '/telegram/send') {\n const { chat_id, message, parse_mode = 'HTML' } = req.body || {};\n if (!chat_id || !message) return err(res, 'Missing: chat_id and message', 400);\n const botToken = process.env.TELEGRAM_BOT_TOKEN;\n if (!botToken) return err(res, 'Set TELEGRAM_BOT_TOKEN in Vercel environment variables. Get from @BotFather on Telegram.', 400);\n try {\n const r = await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, { chat_id, text: message, parse_mode }, { timeout: 15000 });\n return ok(res, { success: true, message_id: r.data.result?.message_id, chat_id, status: 'sent' });\n } catch (e) { return err(res, e.response?.data?.description || e.message); }\n }\n\n if (path === '/telegram/file') {\n const { chat_id, file_url, caption = '' } = req.body || {};\n if (!chat_id || !file_url) return err(res, 'Missing: chat_id and file_url', 400);\n const botToken = process.env.TELEGRAM_BOT_TOKEN;\n if (!botToken) return err(res, 'Set TELEGRAM_BOT_TOKEN in Vercel environment variables.', 400);\n try {\n const ext = file_url.split('.').pop().split('?')[0].toLowerCase();\n let endpoint = 'sendDocument', key = 'document';\n if (['jpg','jpeg','png','webp'].includes(ext)) { endpoint = 'sendPhoto'; key = 'photo'; }\n else if (['mp4','mov','avi'].includes(ext)) { endpoint = 'sendVideo'; key = 'video'; }\n else if (['mp3','ogg','wav'].includes(ext)) { endpoint = 'sendAudio'; key = 'audio'; }\n const r = await axios.post(`https://api.telegram.org/bot${botToken}/${endpoint}`, { chat_id, [key]: file_url, caption }, { timeout: 15000 });\n return ok(res, { success: true, message_id: r.data.result?.message_id, status: 'sent' });\n } catch (e) { return err(res, e.response?.data?.description || e.message); }\n }\n\n // ── CLOUD STORAGE ─────────────────────────────────────────────────\n if (path === '/storage/upload') {\n const { base64, filename, folder = 'uploads' } = req.body || {};\n if (!base64 || !filename) return err(res, 'Missing: base64 (file data) and filename', 400);\n const cloudName = process.env.CLOUDINARY_CLOUD_NAME;\n const apiKey = process.env.CLOUDINARY_API_KEY;\n const apiSecret = process.env.CLOUDINARY_API_SECRET;\n if (!cloudName || !apiKey || !apiSecret) return err(res, 'Set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY and CLOUDINARY_API_SECRET. Free at cloudinary.com', 400);\n try {\n const timestamp = Math.floor(Date.now() / 1000);\n const crypto = require('crypto');\n const sig = crypto.createHash('sha1').update(`folder=${folder}&public_id=${filename}×tamp=${timestamp}${apiSecret}`).digest('hex');\n const FormData = require('form-data');\n const form = new FormData();\n form.append('file', base64);\n form.append('public_id', filename);\n form.append('folder', folder);\n form.append('timestamp', timestamp);\n form.append('api_key', apiKey);\n form.append('signature', sig);\n const r = await axios.post(`https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`, form, { headers: form.getHeaders(), timeout: 60000 });\n return ok(res, { success: true, url: r.data.secure_url, public_id: r.data.public_id, format: r.data.format, size: r.data.bytes, width: r.data.width || null, height: r.data.height || null });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n if (path === '/storage/delete') {\n const { public_id } = req.body || {};\n if (!public_id) return err(res, 'Missing: public_id', 400);\n const cloudName = process.env.CLOUDINARY_CLOUD_NAME;\n const apiKey = process.env.CLOUDINARY_API_KEY;\n const apiSecret = process.env.CLOUDINARY_API_SECRET;\n if (!cloudName || !apiKey || !apiSecret) return err(res, 'Set CLOUDINARY credentials in Vercel environment variables.', 400);\n try {\n const timestamp = Math.floor(Date.now() / 1000);\n const crypto = require('crypto');\n const sig = crypto.createHash('sha1').update(`public_id=${public_id}×tamp=${timestamp}${apiSecret}`).digest('hex');\n const r = await axios.post(`https://api.cloudinary.com/v1_1/${cloudName}/image/destroy`, { public_id, timestamp, api_key: apiKey, signature: sig }, { timeout: 15000 });\n return ok(res, { success: r.data.result === 'ok', public_id });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── PHONE LOOKUP ───────────────────────────────────────────────────\n if (path === '/phone/lookup') {\n const { number } = req.query;\n if (!number) return err(res, 'Missing: number (include country code e.g. +263771234567)', 400);\n try {\n const r = await axios.get(`https://phonevalidation.abstractapi.com/v1/?api_key=${process.env.ABSTRACTAPI_KEY || 'demo'}&phone=${number.replace(/\\s/g, '')}`, { timeout: 10000 });\n return ok(res, { number: r.data.phone, valid: r.data.valid, country: r.data.country?.name, country_code: r.data.country?.calling_code, location: r.data.location, carrier: r.data.carrier, line_type: r.data.line_type, international_format: r.data.format?.international });\n } catch (e) { return err(res, 'Phone lookup requires ABSTRACTAPI_KEY. Free at abstractapi.com'); }\n }\n\n // ── EMAIL VALIDATOR ────────────────────────────────────────────────\n if (path === '/email/validate') {\n const { email } = req.query;\n if (!email) return err(res, 'Missing: email', 400);\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n const valid = emailRegex.test(email);\n const domain = email.split('@')[1] || '';\n const disposableDomains = ['mailinator.com','tempmail.com','guerrillamail.com','10minutemail.com','throwaway.email','temp-mail.org'];\n const isDisposable = disposableDomains.includes(domain.toLowerCase());\n return ok(res, { email, valid, format_valid: valid, domain, is_disposable: isDisposable, is_free: ['gmail.com','yahoo.com','outlook.com','hotmail.com','icloud.com'].includes(domain.toLowerCase()) });\n }\n\n // ── PASTEBIN / TEXT STORAGE ────────────────────────────────────────\n if (path === '/paste/create') {\n const { content, title = 'Untitled', expiry = 'never' } = req.body || {};\n if (!content) return err(res, 'Missing: content', 400);\n try {\n const db = await getDB();\n const id = Math.random().toString(36).slice(2, 10);\n const expiryMap = { '1h': 3600000, '24h': 86400000, '7d': 604800000, 'never': null };\n const expireAt = expiryMap[expiry] ? new Date(Date.now() + expiryMap[expiry]) : null;\n await db.collection('pastes').doc(id).set({ content, title, created_at: new Date(), expire_at: expireAt, views: 0 });\n return ok(res, { id, title, url: `https://blacknodezw.zone.id/api/paste/${id}`, expiry, created_at: new Date().toISOString() });\n } catch (e) { return err(res, e.message); }\n }\n\n const pasteMatch = path.match(/^\\/paste\\/(.+)$/);\n if (pasteMatch) {\n try {\n const db = await getDB();\n const snap = await db.collection('pastes').doc(pasteMatch[1]).get();\n if (!snap.exists) return err(res, 'Paste not found.', 404);\n const data = snap.data();\n if (data.expire_at && new Date() > data.expire_at.toDate()) { await snap.ref.delete(); return err(res, 'Paste has expired.', 404); }\n snap.ref.update({ views: (data.views || 0) + 1 }).catch(() => {});\n return ok(res, { id: pasteMatch[1], title: data.title, content: data.content, views: (data.views || 0) + 1, created_at: data.created_at?.toDate?.()?.toISOString() });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── RANDOM STUFF ───────────────────────────────────────────────────\n if (path === '/random/password') {\n const { length = '16', numbers = 'true', symbols = 'true', uppercase = 'true' } = req.query;\n const len = Math.min(Math.max(parseInt(length) || 16, 8), 128);\n let chars = 'abcdefghijklmnopqrstuvwxyz';\n if (uppercase === 'true') chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\n if (numbers === 'true') chars += '0123456789';\n if (symbols === 'true') chars += '!@#$%^&*()-_=+[]{}|;:,.<>?';\n const password = Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');\n return ok(res, { password, length: len, strength: len >= 16 && numbers === 'true' && symbols === 'true' ? 'strong' : len >= 12 ? 'medium' : 'weak' });\n }\n\n if (path === '/random/uuid') {\n const { count = '1' } = req.query;\n const n = Math.min(parseInt(count) || 1, 50);\n const uuids = Array.from({ length: n }, () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }));\n return ok(res, { count: n, uuids: n === 1 ? uuids[0] : uuids });\n }\n\n if (path === '/random/color') {\n const hex = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');\n const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);\n return ok(res, { hex, rgb: `rgb(${r},${g},${b})`, r, g, b, hsl: rgbToHsl(r, g, b) });\n }\n\n // ── PINTEREST IMAGE SEARCH ─────────────────────────────────────────\n if (path === '/pinterest/search') {\n const { q, count = '20' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n try {\n const r = await axios.get('https://www.pinterest.com/search/pins/', {\n params: { q, rs: 'typed' },\n headers: {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n 'Accept': 'text/html,application/xhtml+xml',\n 'Accept-Language': 'en-US,en;q=0.9'\n },\n timeout: 15000\n });\n const matches = [...r.data.matchAll(/\"images\":\\{\"236x\":\\{\"url\":\"([^\"]+)\"/g)];\n const hdMatches = [...r.data.matchAll(/\"orig\":\\{\"url\":\"([^\"]+)\"/g)];\n const titleMatches = [...r.data.matchAll(/\"grid_description\":\"([^\"]+)\"/g)];\n const results = matches.slice(0, parseInt(count)).map((m, i) => ({\n thumbnail: m[1].replace(/\\\\/g, ''),\n image: (hdMatches[i]?.[1] || m[1]).replace(/\\\\/g, ''),\n title: titleMatches[i]?.[1] || q\n }));\n if (results.length) return ok(res, { query: q, total: results.length, images: results });\n const imgs = [];\n for (let i = 0; i < Math.min(parseInt(count), 10); i++) {\n const seed = encodeURIComponent(q) + i;\n imgs.push({ image: `https://source.unsplash.com/800x800/?${seed}`, thumbnail: `https://source.unsplash.com/400x400/?${seed}`, title: q });\n }\n return ok(res, { query: q, total: imgs.length, images: imgs, note: 'Pinterest blocked direct scraping. Showing Unsplash results instead.' });\n } catch (e) {\n return err(res, 'Pinterest search failed. Try again.');\n }\n }\n\n // ── DEEPSEEK AI ────────────────────────────────────────────────────\n if (path === '/ai/deepseek') {\n const { message, system = 'You are a helpful AI assistant.', model = 'deepseek-chat' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const dsKey = process.env.DEEPSEEK_API_KEY;\n if (!dsKey) return err(res, 'Set DEEPSEEK_API_KEY in Vercel environment variables. Get free key at platform.deepseek.com', 400);\n try {\n const r = await axios.post('https://api.deepseek.com/chat/completions',\n { model, messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 2048 },\n { headers: { 'Authorization': `Bearer ${dsKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: r.data.model, tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── CLAUDE AI ──────────────────────────────────────────────────────\n if (path === '/ai/claude') {\n const { message, system = 'You are a helpful AI assistant.', model = 'claude-3-haiku-20240307' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const claudeKey = process.env.ANTHROPIC_API_KEY;\n if (!claudeKey) return err(res, 'Set ANTHROPIC_API_KEY in Vercel environment variables. Get key at console.anthropic.com', 400);\n try {\n const r = await axios.post('https://api.anthropic.com/v1/messages',\n { model, max_tokens: 1024, system, messages: [{ role: 'user', content: message }] },\n { headers: { 'x-api-key': claudeKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { message, response: r.data.content[0]?.text || '', model: r.data.model, tokens_used: (r.data.usage?.input_tokens || 0) + (r.data.usage?.output_tokens || 0) });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── GROK AI ────────────────────────────────────────────────────────\n if (path === '/ai/grok') {\n const { message, system = 'You are Grok, a helpful AI assistant.', model = 'grok-beta' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const grokKey = process.env.GROK_API_KEY || process.env.XAI_API_KEY;\n if (!grokKey) return err(res, 'Set GROK_API_KEY in Vercel environment variables. Get key at console.x.ai', 400);\n try {\n const r = await axios.post('https://api.x.ai/v1/chat/completions',\n { model, messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 },\n { headers: { 'Authorization': `Bearer ${grokKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: r.data.model, tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── GEMINI AI ──────────────────────────────────────────────────────\n if (path === '/ai/gemini') {\n const { message, model = 'gemini-1.5-flash' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_KEY;\n if (!geminiKey) return err(res, 'Set GEMINI_API_KEY in Vercel environment variables. Get free key at aistudio.google.com', 400);\n try {\n const r = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${geminiKey}`,\n { contents: [{ parts: [{ text: message }] }], generationConfig: { maxOutputTokens: 1024 } },\n { headers: { 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n const response = r.data.candidates?.[0]?.content?.parts?.[0]?.text || '';\n return ok(res, { message, response, model, tokens_used: r.data.usageMetadata?.totalTokenCount || 0 });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── CHATGPT / OPENAI GPT-4 ───────────────────────────────────────────\n if (path === '/ai/chatgpt') {\n const { message, system = 'You are ChatGPT, a helpful AI assistant.', model = 'gpt-4o-mini' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (!openaiKey) return err(res, 'Set OPENAI_API_KEY in Vercel environment variables. Get key at platform.openai.com', 400);\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions',\n { model, messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 2048 },\n { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: r.data.model, tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── AI VIDEO GENERATOR ─────────────────────────────────────────────\n if (path === '/ai/video') {\n const prompt = req.body?.prompt || req.query?.prompt;\n const duration = parseInt(req.body?.duration || req.query?.duration || '5');\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const stabilityKey = process.env.STABILITY_API_KEY;\n if (stabilityKey) {\n try {\n const r = await axios.post('https://api.stability.ai/v2beta/image-to-video',\n { image: `https://image.pollinations.ai/prompt/${encodeURIComponent(prompt)}?width=1024&height=576&nologo=true`, seed: 0, cfg_scale: 1.8, motion_bucket_id: 127 },\n { headers: { 'Authorization': `Bearer ${stabilityKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n if (r.data.id) {\n return ok(res, { prompt, generation_id: r.data.id, status: 'processing', check_url: `https://blacknodezw.zone.id/api/ai/video/result?id=${r.data.id}`, note: 'Video is being generated. Check check_url in 30-60 seconds.' });\n }\n } catch (_) {}\n }\n const lumaKey = process.env.LUMAAI_API_KEY;\n if (lumaKey) {\n try {\n const r = await axios.post('https://api.lumalabs.ai/dream-machine/v1/generations',\n { prompt, aspect_ratio: '16:9', loop: false },\n { headers: { 'Authorization': `Bearer ${lumaKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { prompt, generation_id: r.data.id, status: r.data.state, model: 'luma-dream-machine', note: 'Video generation started. Takes 1-3 minutes.' });\n } catch (_) {}\n }\n return ok(res, {\n prompt, status: 'no_key',\n message: 'AI video generation requires an API key.',\n options: [\n { provider: 'Stability AI', key_name: 'STABILITY_API_KEY', signup: 'platform.stability.ai', free_credits: true },\n { provider: 'Luma AI', key_name: 'LUMAAI_API_KEY', signup: 'lumalabs.ai/dream-machine', free_credits: true },\n { provider: 'RunwayML', key_name: 'RUNWAY_API_KEY', signup: 'runwayml.com', free_credits: true }\n ],\n note: 'Set any of these keys in Vercel environment variables.'\n });\n }\n\n // ── AI VIDEO RESULT CHECK ────────────────────────────────────────────────\n if (path === '/ai/video/result') {\n const { id } = req.query;\n if (!id) return err(res, 'Missing: id', 400);\n const stabilityKey = process.env.STABILITY_API_KEY;\n if (stabilityKey) {\n try {\n const r = await axios.get(`https://api.stability.ai/v2beta/image-to-video/result/${id}`,\n { headers: { 'Authorization': `Bearer ${stabilityKey}`, Accept: 'video/*' }, responseType: 'arraybuffer', timeout: 30000 }\n );\n if (r.status === 200) {\n const b64 = Buffer.from(r.data).toString('base64');\n return ok(res, { id, status: 'complete', video_base64: `data:video/mp4;base64,${b64}` });\n }\n return ok(res, { id, status: 'processing', note: 'Still generating. Try again in 10 seconds.' });\n } catch (_) {}\n }\n return err(res, 'Set STABILITY_API_KEY to check video generation status.');\n }\n\n // ── AI VISION / IMAGE ANALYSIS ────────────────────────────────────\n if (path === '/ai/vision') {\n const { image_url, prompt = 'Describe this image in detail.' } = req.body || {};\n if (!image_url) return err(res, 'Missing: image_url', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions',\n { model: 'gpt-4o-mini', messages: [{ role: 'user', content: [{ type: 'text', text: prompt }, { type: 'image_url', image_url: { url: image_url } }] }], max_tokens: 1024 },\n { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { image_url, prompt, response: r.data.choices[0]?.message?.content || '', model: 'gpt-4o-mini' });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n const geminiKey = process.env.GEMINI_API_KEY;\n if (geminiKey) {\n try {\n const r = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${geminiKey}`,\n { contents: [{ parts: [{ text: prompt }, { inline_data: { mime_type: 'image/jpeg', data: image_url } }] }] },\n { headers: { 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { image_url, prompt, response: r.data.candidates?.[0]?.content?.parts?.[0]?.text || '', model: 'gemini-1.5-flash' });\n } catch (_) {}\n }\n return err(res, 'Set OPENAI_API_KEY or GEMINI_API_KEY for image analysis.');\n }\n\n // ── AI SUMMARIZER ──────────────────────────────────────────────────\n if (path === '/ai/summarize') {\n const { text, url: pageUrl, length = 'medium' } = req.body || {};\n if (!text && !pageUrl) return err(res, 'Missing: text or url', 400);\n let content = text;\n if (pageUrl && !text) {\n try {\n const r = await axios.get(pageUrl, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 12000 });\n const cheerio = require('cheerio');\n const $ = cheerio.load(r.data);\n $('script,style,nav,footer,header,aside').remove();\n content = $('body').text().replace(/\\s+/g, ' ').trim().slice(0, 8000);\n } catch (e) { return err(res, 'Could not fetch URL content: ' + e.message); }\n }\n const maxWords = { short: 100, medium: 250, long: 500 }[length] || 250;\n const prompt = `Summarize the following text in about ${maxWords} words:\\n\\n${content}`;\n const openaiKey = process.env.OPENAI_API_KEY;\n const groqKey = process.env.GROQ_API_KEY;\n try {\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: prompt }], max_tokens: 600 }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 25000 });\n return ok(res, { summary: r.data.choices[0]?.message?.content || '', length, model: 'gpt-3.5-turbo' });\n }\n if (groqKey) {\n const r = await axios.post('https://api.groq.com/openai/v1/chat/completions', { model: 'llama3-8b-8192', messages: [{ role: 'user', content: prompt }], max_tokens: 600 }, { headers: { 'Authorization': `Bearer ${groqKey}`, 'Content-Type': 'application/json' }, timeout: 20000 });\n return ok(res, { summary: r.data.choices[0]?.message?.content || '', length, model: 'llama3-8b' });\n }\n const r = await axios.get(`https://text.pollinations.ai/${encodeURIComponent(prompt)}`, { timeout: 25000 });\n return ok(res, { summary: typeof r.data === 'string' ? r.data : JSON.stringify(r.data), length, model: 'pollinations' });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── COUNTRY INFO ───────────────────────────────────────────────────\n if (path === '/country') {\n const { name, code } = req.query;\n if (!name && !code) return err(res, 'Missing: name or code', 400);\n try {\n const query = code ? `alpha/${code}` : `name/${encodeURIComponent(name)}`;\n const r = await axios.get(`https://restcountries.com/v3.1/${query}`, { timeout: 10000 });\n const c = Array.isArray(r.data) ? r.data[0] : r.data;\n return ok(res, { name: c.name?.common, official_name: c.name?.official, capital: c.capital?.[0] || '', region: c.region, subregion: c.subregion, population: c.population, area_km2: c.area, languages: Object.values(c.languages || {}), currencies: Object.values(c.currencies || {}).map(cu => ({ name: cu.name, symbol: cu.symbol })), timezones: c.timezones, calling_codes: c.idd?.suffixes?.map(s => (c.idd?.root || '') + s) || [], flag_emoji: c.flag, flag_url: c.flags?.png || '', continent: c.continents?.[0] || '' });\n } catch (e) { return err(res, `Country not found: ${name || code}`); }\n }\n\n // ── NOT FOUND ─────────────────────────────────────────────────────────────\n return res.status(404).json({ status: 404, error: `Endpoint not found: ${path}`, docs: 'https://blacknodezw.zone.id', available_endpoints: ['/tiktok/download','/tiktok/user','/tiktok/search','/tiktok/trending','/youtube/mp3','/youtube/mp4','/youtube/info','/youtube/channel','/instagram/download','/instagram/user','/facebook/download','/twitter/download','/pinterest/download','/spotify/track','/image/search','/image/generate','/image/removebg','/ai/chat','/ai/generate','/ai/tts','/ai/translate','/ai/music','/weather','/currency/convert','/qrcode/generate','/ip/lookup','/url/shorten','/news','/dictionary','/github/user','/screenshot','/fun','/movie/search','/movie/details','/movie/trending','/whatsapp/send','/telegram/send','/telegram/file','/storage/upload','/phone/lookup','/email/validate','/paste/create','/random/password','/random/uuid','/random/color'] });\n\n } catch (e) {\n return err(res, e.message);\n }\n};\n\nfunction rgbToHsl(r, g, b) {\n r /= 255; g /= 255; b /= 255;\n const max = Math.max(r, g, b), min = Math.min(r, g, b);\n let h, s, l = (max + min) / 2;\n if (max === min) { h = s = 0; } else {\n const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; }\n h /= 6;\n }\n return `hsl(${Math.round(h * 360)},${Math.round(s * 100)}%,${Math.round(l * 100)}%)`;\n}\n"}, "857dc78a07434858cde763a8deae60ccfaa38e06": {"t": 1785767089.6344402, "n": 82947, "h": "ffdd9bd98bd583bf", "c": "// api/index.js - Black Node ZW - All endpoints unified\nconst axios = require('axios');\nconst QRCode = require('qrcode');\n\nfunction cors(res) {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type,x-api-key,Authorization');\n}\nconst ok = (res, data) => res.status(200).json({ status: 200, data });\nconst err = (res, msg, code = 500) => res.status(code).json({ status: code, error: msg });\n\n// ── FIREBASE ──────────────────────────────────────────────────────────────────\nlet _db = null;\nasync function getDB() {\n if (_db) return _db;\n const { initializeApp, getApps, cert } = require('firebase-admin/app');\n const { getFirestore } = require('firebase-admin/firestore');\n if (!getApps().length) {\n if (process.env.FIREBASE_SERVICE_ACCOUNT) {\n initializeApp({ credential: cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT)) });\n } else { initializeApp({ projectId: 'apiblacknodezw' }); }\n }\n _db = getFirestore();\n return _db;\n}\n\nasync function authCheck(req, res) {\n if (req.method === 'OPTIONS') { res.status(200).end(); return false; }\n const key = req.headers['x-api-key'] || req.query.api_key || req.query.apikey || req.query.key;\n if (!key) { err(res, 'Missing API key. Add x-api-key header or ?api_key=YOUR_KEY to URL.', 401); return false; }\n try {\n const db = await getDB();\n const snap = await db.collection('users').where('apiKey', '==', key).limit(1).get();\n if (snap.empty) { err(res, 'Invalid API key.', 403); return false; }\n const user = snap.docs[0].data();\n const limit = user.plan === 'pro' ? 50000 : 1000;\n if ((user.requests || 0) >= limit) { err(res, 'Monthly limit reached. Upgrade to Pro.', 429); return false; }\n snap.docs[0].ref.update({ requests: (user.requests || 0) + 1 }).catch(() => {});\n return true;\n } catch (e) { return true; }\n}\n\n// ── HELPERS ───────────────────────────────────────────────────────────────────\n// Extract YouTube video ID from any YouTube URL including Shorts\nfunction getYTId(url) {\n return url.match(/(?:v=|youtu\\.be\\/|embed\\/|shorts\\/|\\/v\\/)([^&\\n?#]+)/)?.[1] || null;\n}\n\n// Cobalt download helper with retries\nasync function cobaltDownload(url, opts = {}) {\n try {\n const resp = await axios.post('https://api.cobalt.tools/api/json',\n { url, ...opts },\n { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, timeout: 25000 }\n );\n return resp.data;\n } catch (e) {\n return null;\n }\n}\n\n// ── MAIN ROUTER ───────────────────────────────────────────────────────────────\nmodule.exports = async (req, res) => {\n cors(res);\n if (req.method === 'OPTIONS') return res.status(200).end();\n\n const path = req.url.replace(/\\?.*/, '').replace(/^\\/api/, '');\n\n // No auth routes\n if (path === '' || path === '/' || path === '/status') {\n return res.json({ status: 'online', name: 'Black Node ZW API', version: '1.0.0', base_url: 'https://api-blacknode-zw.vercel.app', endpoints: 40, timestamp: new Date().toISOString() });\n }\n\n // Short URL redirect\n const shortMatch = path.match(/^\\/s\\/(.+)$/);\n if (shortMatch) {\n try {\n const db = await getDB();\n const snap = await db.collection('short_urls').doc(shortMatch[1]).get();\n if (!snap.exists) return res.status(404).send('Link not found');\n snap.ref.update({ clicks: (snap.data().clicks || 0) + 1 }).catch(() => {});\n return res.redirect(301, snap.data().url);\n } catch (e) { return res.redirect('https://api-blacknode-zw.vercel.app'); }\n }\n\n if (!(await authCheck(req, res))) return;\n\n try {\n\n // ── TIKTOK ──────────────────────────────────────────────────────────────\n if (path === '/tiktok/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const decodedUrl = decodeURIComponent(url);\n try {\n const r = await axios.post('https://www.tikwm.com/api/',\n new URLSearchParams({ url: decodedUrl, hd: '1' }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 }\n );\n const d = r.data?.data;\n if (r.data?.code === 0 && d) {\n return ok(res, { title: d.title || '', author: '@' + (d.author?.unique_id || ''), author_name: d.author?.nickname || '', thumbnail: d.cover || '', download_url_hd: d.hdplay || d.play || '', download_url_sd: d.play || '', music_url: d.music || '', duration: d.duration || 0, play_count: d.play_count || 0, like_count: d.digg_count || 0 });\n }\n } catch (e) {}\n // Fallback: cobalt\n const cd = await cobaltDownload(decodedUrl);\n if (cd?.status === 'stream' || cd?.status === 'redirect') {\n return ok(res, { title: 'TikTok Video', download_url_hd: cd.url, download_url_sd: cd.url });\n }\n return err(res, 'Could not download. Make sure the video is public and the URL is correct.');\n }\n\n if (path === '/tiktok/user') {\n const { username, include_videos = 'false', video_count = '10' } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n const r = await axios.post('https://www.tikwm.com/api/user/info',\n new URLSearchParams({ unique_id: clean }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 }\n );\n if (r.data?.code !== 0) return err(res, 'User not found or account is private.');\n const u = r.data.data?.user || {}, s = r.data.data?.stats || {};\n const result = { username: u.uniqueId || clean, nickname: u.nickname || '', bio: u.signature || '', avatar: u.avatarLarger || '', verified: u.verified || false, private: u.privateAccount || false, followers: s.followerCount || 0, following: s.followingCount || 0, likes: s.heartCount || 0, video_count: s.videoCount || 0, profile_url: `https://www.tiktok.com/@${clean}` };\n if (include_videos === 'true') {\n try {\n const vr = await axios.post('https://www.tikwm.com/api/user/posts', new URLSearchParams({ unique_id: clean, count: Math.min(parseInt(video_count), 30), cursor: '0' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (vr.data?.code === 0) result.recent_videos = (vr.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0 }));\n } catch (_) {}\n }\n return ok(res, result);\n }\n\n if (path === '/tiktok/search') {\n const { q, count = '20', cursor = '0' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const r = await axios.post('https://www.tikwm.com/api/feed/search', new URLSearchParams({ keywords: q, count: Math.min(parseInt(count), 50), cursor }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 });\n if (r.data?.code !== 0) return err(res, 'Search failed. Try again.');\n return ok(res, { query: q, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' } })) });\n }\n\n if (path === '/tiktok/trending') {\n const { region = 'US', count = '20' } = req.query;\n const r = await axios.post('https://www.tikwm.com/api/feed/list', new URLSearchParams({ region, count: Math.min(parseInt(count), 50) }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 });\n if (r.data?.code !== 0) return err(res, 'Could not fetch trending.');\n return ok(res, { region, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', play_count: v.play_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' } })) });\n }\n\n // ── YOUTUBE ──────────────────────────────────────────────────────────────\n if (path === '/youtube/mp3' || path === '/youtube/mp4') {\n const { url, quality = '720' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = getYTId(url);\n if (!videoId) return err(res, 'Invalid YouTube URL. Supported: youtube.com/watch, youtu.be, youtube.com/shorts', 400);\n const fullUrl = `https://www.youtube.com/watch?v=${videoId}`;\n const isAudio = path === '/youtube/mp3';\n\n // Get title\n let title = 'YouTube Video', channel = 'YouTube';\n try {\n const oe = await axios.get(`https://www.youtube.com/oembed?url=${encodeURIComponent(fullUrl)}&format=json`, { timeout: 6000 });\n title = oe.data.title || title;\n channel = oe.data.author_name || channel;\n } catch (_) {}\n\n const thumbnail = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;\n\n // Try cobalt first\n const cd = await cobaltDownload(fullUrl, isAudio ? { isAudioOnly: true, aFormat: 'mp3', audioBitrate: '320' } : { vQuality: quality });\n if (cd?.status === 'stream' || cd?.status === 'redirect') {\n return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.url, quality: isAudio ? '320kbps' : quality + 'p', format: isAudio ? 'mp3' : 'mp4' });\n }\n if (cd?.status === 'picker' && cd.picker?.length) {\n return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.picker[0].url, quality: quality + 'p', format: 'mp4' });\n }\n\n // Fallback: yt-dlp via loader.to free API\n try {\n const loaderResp = await axios.get(`https://loader.to/api/button/?url=${encodeURIComponent(fullUrl)}&f=${isAudio ? 'mp3' : 'mp4-' + quality}`, { timeout: 15000, headers: { 'User-Agent': 'Mozilla/5.0' } });\n if (loaderResp.data) {\n return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: `https://loader.to/api/download/?url=${encodeURIComponent(fullUrl)}&f=${isAudio ? 'mp3' : 'mp4'}`, format: isAudio ? 'mp3' : 'mp4', note: 'Use download_url in browser to download' });\n }\n } catch (_) {}\n\n return err(res, 'Could not fetch. cobalt.tools may be temporarily down. Try again in a moment.');\n }\n\n if (path === '/youtube/info') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = getYTId(url);\n if (!videoId) return err(res, 'Invalid YouTube URL', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (ytKey) {\n try {\n const r = await axios.get('https://www.googleapis.com/youtube/v3/videos', { params: { key: ytKey, id: videoId, part: 'snippet,statistics' }, timeout: 10000 });\n const item = r.data.items?.[0];\n if (item) return ok(res, { video_id: videoId, title: item.snippet.title, channel: item.snippet.channelTitle, thumbnail: item.snippet.thumbnails?.high?.url || '', views: parseInt(item.statistics.viewCount) || 0, likes: parseInt(item.statistics.likeCount) || 0, comments: parseInt(item.statistics.commentCount) || 0, published_at: item.snippet.publishedAt });\n } catch (_) {}\n }\n const r = await axios.get(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`, { timeout: 8000 });\n return ok(res, { video_id: videoId, title: r.data.title, channel: r.data.author_name, thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`, url: `https://www.youtube.com/watch?v=${videoId}` });\n }\n\n if (path === '/youtube/channel') {\n const { channel } = req.query;\n if (!channel) return err(res, 'Missing: channel', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (!ytKey) return err(res, 'Set YOUTUBE_API_KEY in Vercel environment variables for this endpoint.', 400);\n const search = await axios.get('https://www.googleapis.com/youtube/v3/search', { params: { key: ytKey, q: channel.replace(/^@/, ''), type: 'channel', part: 'id', maxResults: 1 }, timeout: 10000 });\n const channelId = search.data.items?.[0]?.id?.channelId;\n if (!channelId) return err(res, 'Channel not found.', 404);\n const stats = await axios.get('https://www.googleapis.com/youtube/v3/channels', { params: { key: ytKey, id: channelId, part: 'snippet,statistics' }, timeout: 10000 });\n const ch = stats.data.items?.[0];\n if (!ch) return err(res, 'Channel not found.', 404);\n return ok(res, { channel_id: channelId, title: ch.snippet.title, description: ch.snippet.description?.slice(0, 300), avatar: ch.snippet.thumbnails?.high?.url || '', subscribers: parseInt(ch.statistics.subscriberCount) || 0, total_views: parseInt(ch.statistics.viewCount) || 0, video_count: parseInt(ch.statistics.videoCount) || 0 });\n }\n\n // ── INSTAGRAM ────────────────────────────────────────────────────────────\n if (path === '/instagram/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const decodedUrl = decodeURIComponent(url);\n // Method 1: cobalt\n const cd = await cobaltDownload(decodedUrl);\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { type: 'video', download_url: cd.url });\n if (cd?.status === 'picker') return ok(res, { type: 'gallery', items: cd.picker.map(p => ({ type: p.type, url: p.url })) });\n // Method 2: snapinsta\n try {\n const snapResp = await axios.post('https://snapinsta.app/action_download.php',\n new URLSearchParams({ url: decodedUrl }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0', Referer: 'https://snapinsta.app/', Origin: 'https://snapinsta.app' }, timeout: 20000 }\n );\n const html = typeof snapResp.data === 'string' ? snapResp.data : '';\n const videoMatch = html.match(/href=\"(https:\\/\\/[^\"]*\\.mp4[^\"]*)\"/);\n const imgMatch = html.match(/src=\"(https:\\/\\/[^\"]*\\.jpg[^\"]*)\"/);\n if (videoMatch) return ok(res, { type: 'video', download_url: videoMatch[1] });\n if (imgMatch) return ok(res, { type: 'image', download_url: imgMatch[1] });\n } catch (_) {}\n // Method 3: instagramsave\n try {\n const r = await axios.post('https://v3.saveinsta.app/api/ajaxSearch',\n new URLSearchParams({ q: decodedUrl, t: 'media', lang: 'en' }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 }\n );\n if (r.data?.data) {\n const d = r.data.data;\n if (d.medias?.length) return ok(res, { type: d.type || 'video', items: d.medias.map(m => ({ url: m.url, quality: m.quality || 'hd', extension: m.extension || 'mp4' })), download_url: d.medias[0]?.url });\n }\n } catch (_) {}\n return err(res, 'Could not download Instagram media. The post may be private or Instagram is blocking scrapers. Try again.');\n }\n\n if (path === '/instagram/user') {\n const { username } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n // Method 1: Instagram API\n try {\n const r = await axios.get(`https://www.instagram.com/api/v1/users/web_profile_info/?username=${clean}`, {\n headers: { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15', 'X-IG-App-ID': '936619743392459', Referer: `https://www.instagram.com/${clean}/`, Accept: '*/*' },\n timeout: 15000\n });\n const u = r.data?.data?.user;\n if (u) return ok(res, { username: u.username, full_name: u.full_name || '', bio: u.biography || '', avatar: u.profile_pic_url_hd || u.profile_pic_url || '', verified: u.is_verified || false, private: u.is_private || false, followers: u.edge_followed_by?.count || 0, following: u.edge_follow?.count || 0, posts: u.edge_owner_to_timeline_media?.count || 0 });\n } catch (_) {}\n // Method 2: page scrape\n try {\n const r = await axios.get(`https://www.instagram.com/${clean}/`, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', Accept: 'text/html' }, timeout: 12000 });\n const desc = r.data.match(/<meta name=\"description\" content=\"([^\"]+)\"/)?.[1] || '';\n const fMatch = desc.match(/([\\d,.KMB]+)\\s*Followers/i);\n const fgMatch = desc.match(/([\\d,.KMB]+)\\s*Following/i);\n const pMatch = desc.match(/([\\d,.KMB]+)\\s*Posts/i);\n const parseK = s => { if (!s) return 0; s = s.replace(/,/g, '').toUpperCase(); if (s.includes('M')) return Math.round(parseFloat(s) * 1e6); if (s.includes('K')) return Math.round(parseFloat(s) * 1e3); return parseInt(s) || 0; };\n return ok(res, { username: clean, followers: fMatch ? parseK(fMatch[1]) : 0, following: fgMatch ? parseK(fgMatch[1]) : 0, posts: pMatch ? parseK(pMatch[1]) : 0, profile_url: `https://www.instagram.com/${clean}/`, note: 'Instagram limits profile data. Set RAPIDAPI_KEY for full data.' });\n } catch (e) {\n return err(res, 'Instagram profile fetch failed. Instagram actively blocks scrapers. Try again later.');\n }\n }\n\n // ── OTHER DOWNLOADERS ────────────────────────────────────────────────────\n if (path === '/facebook/download') {\n const { url, quality = 'hd' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const decodedUrl = decodeURIComponent(url);\n // Method 1: cobalt\n const cd = await cobaltDownload(decodedUrl);\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { download_url: cd.url, quality });\n if (cd?.status === 'picker' && cd.picker?.length) {\n const pick = quality === 'hd' ? cd.picker[0] : cd.picker[cd.picker.length - 1];\n return ok(res, { download_url: pick.url, quality, all_qualities: cd.picker.map(p => ({ url: p.url, quality: p.quality || 'unknown' })) });\n }\n // Method 2: getfvid scrape\n try {\n const r = await axios.post('https://www.getfvid.com/downloader',\n new URLSearchParams({ url: decodedUrl }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0', Referer: 'https://www.getfvid.com/', Origin: 'https://www.getfvid.com' }, timeout: 20000 }\n );\n const hdMatch = r.data.match(/href=\"(https:\\/\\/[^\"]+\\.mp4[^\"]*)\"\\s[^>]*>HD/i);\n const sdMatch = r.data.match(/href=\"(https:\\/\\/[^\"]+\\.mp4[^\"]*)\"\\s[^>]*>SD/i);\n const anyMatch = r.data.match(/href=\"(https:\\/\\/video[^\\.]*\\.facebook[^\"]+)\"/);\n const dlUrl = (quality === 'hd' ? hdMatch?.[1] : sdMatch?.[1]) || hdMatch?.[1] || sdMatch?.[1] || anyMatch?.[1];\n if (dlUrl) return ok(res, { download_url: dlUrl, quality: hdMatch ? 'hd' : 'sd' });\n } catch (_) {}\n // Method 3: fdown.net\n try {\n const r = await axios.post('https://fdown.net/download.php',\n new URLSearchParams({ URLz: decodedUrl }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0', Referer: 'https://fdown.net/' }, timeout: 20000 }\n );\n const hdMatch = r.data.match(/id=\"hdlink\"\\s+href=\"([^\"]+)\"/);\n const sdMatch = r.data.match(/id=\"sdlink\"\\s+href=\"([^\"]+)\"/);\n if (hdMatch || sdMatch) return ok(res, { download_url_hd: hdMatch?.[1] || '', download_url_sd: sdMatch?.[1] || '', download_url: hdMatch?.[1] || sdMatch?.[1] });\n } catch (_) {}\n return err(res, 'Could not download Facebook video. Make sure the video is public.');\n }\n\n if (path === '/twitter/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const decodedUrl = decodeURIComponent(url);\n const cd = await cobaltDownload(decodedUrl);\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { download_url: cd.url, quality: 'hd' });\n if (cd?.status === 'picker' && cd.picker?.length) return ok(res, { items: cd.picker.map(p => ({ quality: p.quality || 'unknown', url: p.url })) });\n // Fallback: twitsave\n try {\n const r = await axios.get(`https://twitsave.com/info?url=${encodeURIComponent(decodedUrl)}`, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n const match = r.data.match(/downloadFile\\(\"(https:\\/\\/[^\"]+\\.mp4[^\"]*)\"/);\n if (match) return ok(res, { download_url: match[1] });\n } catch (_) {}\n return err(res, 'Could not download. Make sure the tweet has a video and is public.');\n }\n\n if (path === '/pinterest/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n try {\n const cheerio = require('cheerio');\n const r = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0', Accept: 'text/html' }, maxRedirects: 5, timeout: 15000 });\n const $ = cheerio.load(r.data);\n const videoUrl = $('meta[property=\"og:video\"]').attr('content') || '';\n const imageUrl = $('meta[property=\"og:image\"]').attr('content') || '';\n if (!videoUrl && !imageUrl) return err(res, 'No media found. Make sure the pin is public.');\n const hdImage = imageUrl.replace(/\\/\\d+x\\//, '/originals/');\n return ok(res, { type: videoUrl ? 'video' : 'image', title: $('meta[property=\"og:title\"]').attr('content') || '', download_url: videoUrl || hdImage, thumbnail: imageUrl, hd_image: hdImage });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── IMAGE ────────────────────────────────────────────────────────────────\n if (path === '/image/search') {\n const { q, count = '10' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const gKey = process.env.GOOGLE_SEARCH_API_KEY, cx = process.env.GOOGLE_SEARCH_CX;\n if (gKey && cx) {\n try {\n const r = await axios.get('https://www.googleapis.com/customsearch/v1', { params: { key: gKey, cx, q, searchType: 'image', num: Math.min(parseInt(count), 10) }, timeout: 10000 });\n return ok(res, { query: q, images: (r.data.items || []).map(i => ({ url: i.link, thumbnail: i.image?.thumbnailLink || '', title: i.title, source: i.image?.contextLink || '' })) });\n } catch (_) {}\n }\n // Fallback: Unsplash public API (no key needed)\n try {\n const r = await axios.get(`https://source.unsplash.com/featured/?${encodeURIComponent(q)}`, { maxRedirects: 0, timeout: 8000, validateStatus: s => s < 400 });\n const finalUrl = r.headers?.location || r.request?.res?.responseUrl || '';\n if (finalUrl) return ok(res, { query: q, images: [{ url: finalUrl, thumbnail: finalUrl, title: q, source: 'unsplash.com' }], note: 'Set GOOGLE_SEARCH_API_KEY for more results' });\n } catch (_) {}\n // Fallback: Pixabay free API\n try {\n const r = await axios.get('https://pixabay.com/api/', { params: { key: process.env.PIXABAY_API_KEY || '44556677-abc123def456', q, image_type: 'photo', per_page: Math.min(parseInt(count), 20) }, timeout: 10000 });\n if (r.data?.hits?.length) return ok(res, { query: q, images: r.data.hits.map(i => ({ url: i.largeImageURL || i.webformatURL, thumbnail: i.previewURL, title: i.tags, source: 'pixabay.com' })) });\n } catch (_) {}\n return err(res, 'Image search failed. Set GOOGLE_SEARCH_API_KEY and GOOGLE_SEARCH_CX in Vercel environment variables for reliable image search.');\n }\n\n if (path === '/image/generate') {\n const prompt = req.body?.prompt || req.query?.prompt;\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const style = req.body?.style || req.query?.style || 'realistic';\n const styleMap = { anime: 'anime style,', art: 'digital art,', cartoon: 'cartoon,', sketch: 'pencil sketch,' };\n const fullPrompt = ((styleMap[style] || '') + ' ' + prompt).trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/images/generations', { model: 'dall-e-3', prompt: fullPrompt, n: 1, size: '1024x1024' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 60000 });\n return ok(res, { prompt: fullPrompt, image_url: r.data.data[0]?.url, model: 'dall-e-3' });\n } catch (_) {}\n }\n const seed = Math.floor(Math.random() * 1000000);\n const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(fullPrompt)}?width=1024&height=1024&model=flux&seed=${seed}&nologo=true`;\n return ok(res, { prompt: fullPrompt, image_url: imageUrl, model: 'flux', seed, note: 'Open image_url in browser to view the generated image' });\n }\n\n if (path === '/image/removebg') {\n const imageUrl = req.body?.url || req.query?.url;\n if (!imageUrl) return err(res, 'Missing: url', 400);\n const rbKey = process.env.REMOVEBG_API_KEY;\n if (rbKey) {\n try {\n const FormData = require('form-data');\n const form = new FormData();\n form.append('image_url', imageUrl);\n form.append('size', 'auto');\n const r = await axios.post('https://api.remove.bg/v1.0/removebg', form, { headers: { ...form.getHeaders(), 'X-Api-Key': rbKey }, responseType: 'arraybuffer', timeout: 30000 });\n const b64 = Buffer.from(r.data).toString('base64');\n return ok(res, { original: imageUrl, result_base64: `data:image/png;base64,${b64}`, format: 'png', provider: 'remove.bg' });\n } catch (_) {}\n }\n // Free fallback: bg.removal.ai\n try {\n const r = await axios.post('https://api.photoroom.com/v1/segment', { image_url: imageUrl }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });\n if (r.data?.image_url) return ok(res, { original: imageUrl, result_url: r.data.image_url, format: 'png', provider: 'photoroom' });\n } catch (_) {}\n return err(res, 'Background removal requires REMOVEBG_API_KEY. Get a free key at remove.bg/api');\n }\n\n // ── AI ───────────────────────────────────────────────────────────────────\n if (path === '/ai/chat') {\n const { message, system = 'You are a helpful AI assistant.', model = 'default' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 25000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'gpt-3.5-turbo', tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (_) {}\n }\n // Free fallback: Groq (very fast free AI)\n const groqKey = process.env.GROQ_API_KEY;\n if (groqKey) {\n try {\n const r = await axios.post('https://api.groq.com/openai/v1/chat/completions', { model: 'llama3-8b-8192', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Authorization': `Bearer ${groqKey}`, 'Content-Type': 'application/json' }, timeout: 20000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'llama3-8b (groq)', tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (_) {}\n }\n // Final fallback: DeepInfra\n try {\n const r = await axios.post('https://api.deepinfra.com/v1/openai/chat/completions', { model: 'meta-llama/Meta-Llama-3-8B-Instruct', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'llama-3-8b' });\n } catch (e) { return err(res, 'AI service temporarily unavailable. Set OPENAI_API_KEY or GROQ_API_KEY (free at groq.com) for reliable AI chat.'); }\n }\n\n if (path === '/ai/generate') {\n const { prompt, type = 'default', length = 'medium', tone = 'professional' } = req.body || {};\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const typeMap = { article: 'Write a detailed article about:', code: 'Write clean well-commented code for:', story: 'Write a creative short story about:', email: 'Write a professional email for:', essay: 'Write an essay about:', caption: 'Write social media captions for:' };\n const maxTokens = { short: 300, medium: 700, long: 1400 }[length] || 700;\n const fullPrompt = `${typeMap[type] || ''} ${prompt}. Tone: ${tone}`.trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: fullPrompt }], max_tokens: maxTokens }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 });\n const output = r.data.choices[0]?.message?.content || '';\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length });\n } catch (_) {}\n }\n // Fallback: Pollinations text\n try {\n const r = await axios.get(`https://text.pollinations.ai/${encodeURIComponent(fullPrompt)}`, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 30000 });\n const output = typeof r.data === 'string' ? r.data : JSON.stringify(r.data);\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length, model: 'pollinations' });\n } catch (e) { return err(res, 'AI generation timed out. Set OPENAI_API_KEY or GROQ_API_KEY for faster responses.'); }\n }\n\n if (path === '/ai/tts') {\n const text = req.body?.text || req.query?.text;\n const voice = req.body?.voice || req.query?.voice || 'Brian';\n if (!text) return err(res, 'Missing: text', 400);\n if (text.length > 3000) return err(res, 'Text too long. Max 3000 characters.', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/audio/speech', { model: 'tts-1', input: text, voice: 'alloy' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, responseType: 'arraybuffer', timeout: 25000 });\n res.setHeader('Content-Type', 'audio/mpeg');\n return res.send(Buffer.from(r.data));\n } catch (_) {}\n }\n const voices = ['Brian','Amy','Emma','Matthew','Joanna','Joey','Kendra','Kimberly','Salli','Justin'];\n const sel = voices.includes(voice) ? voice : 'Brian';\n const audioUrl = `https://api.streamelements.com/kappa/v2/speech?voice=${sel}&text=${encodeURIComponent(text)}`;\n return ok(res, { text, voice: sel, audio_url: audioUrl, format: 'mp3', note: 'Open audio_url in browser or use in an audio player' });\n }\n\n if (path === '/ai/translate') {\n const { text, to, from = 'auto' } = req.query;\n if (!text) return err(res, 'Missing: text', 400);\n if (!to) return err(res, 'Missing: to (target language code e.g. es, fr, sn)', 400);\n try {\n const r = await axios.get('https://translate.googleapis.com/translate_a/single', { params: { client: 'gtx', sl: from === 'auto' ? 'auto' : from, tl: to, dt: 't', q: text }, headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data[0]?.map(s => s[0]).join('') || '', from: r.data[2] || from, to });\n } catch (_) {\n try {\n const r = await axios.get('https://api.mymemory.translated.net/get', { params: { q: text, langpair: `${from}|${to}` }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data.responseData.translatedText, from, to });\n } catch (e) { return err(res, 'Translation failed. Try again.'); }\n }\n }\n\n // NEW: AI Music Generator\n if (path === '/ai/music') {\n const prompt = req.body?.prompt || req.query?.prompt;\n const style = req.body?.style || req.query?.style || 'pop';\n if (!prompt) return err(res, 'Missing: prompt', 400);\n // Uses MusicGen via Hugging Face\n const hfKey = process.env.HUGGINGFACE_API_KEY;\n if (hfKey) {\n try {\n const r = await axios.post('https://api-inference.huggingface.co/models/facebook/musicgen-small',\n { inputs: `${style} music: ${prompt}` },\n { headers: { 'Authorization': `Bearer ${hfKey}`, 'Content-Type': 'application/json' }, responseType: 'arraybuffer', timeout: 60000 }\n );\n const b64 = Buffer.from(r.data).toString('base64');\n return ok(res, { prompt, style, audio_base64: `data:audio/wav;base64,${b64}`, format: 'wav', model: 'musicgen-small', note: 'Set HUGGINGFACE_API_KEY for this endpoint' });\n } catch (_) {}\n }\n // Fallback: Suno AI public URL\n return ok(res, { prompt, style, note: 'Set HUGGINGFACE_API_KEY for AI music generation. Free at huggingface.co', alternative: 'Try suno.com or udio.com for free AI music generation' });\n }\n\n // ── WEATHER ──────────────────────────────────────────────────────────────\n if (path === '/weather') {\n const { city, units = 'metric' } = req.query;\n if (!city) return err(res, 'Missing: city', 400);\n try {\n const geo = await axios.get('https://nominatim.openstreetmap.org/search', { params: { q: city, format: 'json', limit: 1 }, headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 10000 });\n if (!geo.data?.length) return err(res, `City not found: \"${city}\"`, 404);\n const { lat, lon, display_name } = geo.data[0];\n const w = await axios.get('https://api.open-meteo.com/v1/forecast', { params: { latitude: lat, longitude: lon, current: 'temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,precipitation', daily: 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum', temperature_unit: units === 'imperial' ? 'fahrenheit' : 'celsius', wind_speed_unit: units === 'imperial' ? 'mph' : 'kmh', timezone: 'auto', forecast_days: 7 }, timeout: 12000 });\n const c = w.data.current, unit = units === 'imperial' ? '°F' : '°C';\n const WMO = { 0:'Clear sky',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Foggy',51:'Light drizzle',61:'Slight rain',63:'Moderate rain',65:'Heavy rain',71:'Slight snow',80:'Showers',95:'Thunderstorm' };\n return ok(res, { city: display_name.split(',')[0], full_location: display_name, coordinates: { lat: parseFloat(lat), lon: parseFloat(lon) }, temperature: `${c.temperature_2m}${unit}`, feels_like: `${c.apparent_temperature}${unit}`, condition: WMO[c.weather_code] || 'Unknown', humidity: `${c.relative_humidity_2m}%`, wind_speed: `${c.wind_speed_10m} ${units === 'imperial' ? 'mph' : 'km/h'}`, precipitation: `${c.precipitation}mm`, units, forecast: w.data.daily.time.map((d, i) => ({ date: d, condition: WMO[w.data.daily.weather_code[i]] || 'Unknown', high: `${w.data.daily.temperature_2m_max[i]}${unit}`, low: `${w.data.daily.temperature_2m_min[i]}${unit}`, precipitation: `${w.data.daily.precipitation_sum[i]}mm` })) });\n } catch (e) { return err(res, `Weather fetch failed: ${e.message}`); }\n }\n\n // ── CURRENCY ─────────────────────────────────────────────────────────────\n if (path === '/currency/convert') {\n let { from = 'USD', to, amount = '1', action } = req.query;\n // Clean up common user mistakes\n from = from.replace(/[^A-Za-z]/g, '').toUpperCase().slice(0, 3);\n if (!from || from.length < 2) from = 'USD';\n try {\n const r = await axios.get(`https://open.er-api.com/v6/latest/${from}`, { timeout: 10000 });\n if (r.data.result !== 'success') throw new Error('Exchange rate API failed');\n if (action === 'rates') return ok(res, { base: from, rates: r.data.rates, last_updated: r.data.time_last_update_utc });\n if (!to) return err(res, 'Missing: to (target currency code e.g. ZAR, EUR, GBP)', 400);\n to = to.replace(/[^A-Za-z]/g, '').toUpperCase().slice(0, 3);\n const rate = r.data.rates[to];\n if (!rate) return err(res, `Currency not found: \"${to}\". Use standard codes like USD, EUR, ZAR, GBP.`, 400);\n const amt = parseFloat(amount) || 1;\n return ok(res, { from, to, amount: amt, rate, result: parseFloat((amt * rate).toFixed(6)), formatted: `${(amt * rate).toFixed(2)} ${to}`, last_updated: r.data.time_last_update_utc });\n } catch (e) {\n // Fallback: Frankfurter\n try {\n const to2 = (to || 'USD').replace(/[^A-Za-z]/g, '').toUpperCase().slice(0, 3);\n const r = await axios.get(`https://api.frankfurter.app/latest?from=${from}&to=${to2}`, { timeout: 10000 });\n const rate = r.data.rates[to2];\n if (!rate) return err(res, `Currency not found: \"${to2}\"`, 400);\n const amt = parseFloat(amount) || 1;\n return ok(res, { from, to: to2, amount: amt, rate, result: parseFloat((amt * rate).toFixed(6)), formatted: `${(amt * rate).toFixed(2)} ${to2}` });\n } catch (e2) { return err(res, 'Currency conversion failed. Try again.'); }\n }\n }\n\n // ── QR CODE ──────────────────────────────────────────────────────────────\n if (path === '/qrcode/generate') {\n const { data, size = '300', color = '000000', bg = 'ffffff', format = 'json' } = req.query;\n if (!data) return err(res, 'Missing: data', 400);\n const sizeNum = Math.min(Math.max(parseInt(size) || 300, 100), 1000);\n const opts = { width: sizeNum, margin: 2, color: { dark: `#${color.replace('#', '')}`, light: `#${bg.replace('#', '')}` } };\n // Always return base64/JSON by default so tester can display it\n if (format === 'svg') {\n const svg = await QRCode.toString(data, { ...opts, type: 'svg' });\n res.setHeader('Content-Type', 'image/svg+xml');\n return res.send(svg);\n }\n if (format === 'png') {\n res.setHeader('Content-Type', 'image/png');\n return res.send(await QRCode.toBuffer(data, opts));\n }\n // Default: return as JSON with base64\n const dataUrl = await QRCode.toDataURL(data, opts);\n return ok(res, { data, format: 'base64', size: `${sizeNum}x${sizeNum}`, image: dataUrl, note: 'Use format=png or format=svg in URL for direct image response' });\n }\n\n // ── IP LOOKUP ────────────────────────────────────────────────────────────\n if (path === '/ip/lookup') {\n let { ip } = req.query;\n if (!ip) ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || '';\n try {\n const r = await axios.get(`https://ipwho.is/${ip}`, { timeout: 8000 });\n if (r.data.success) return ok(res, { ip: r.data.ip, country: r.data.country, country_code: r.data.country_code, region: r.data.region, city: r.data.city, latitude: r.data.latitude, longitude: r.data.longitude, timezone: r.data.timezone?.id || '', isp: r.data.connection?.isp || '', org: r.data.connection?.org || '' });\n } catch (_) {}\n try {\n const r = await axios.get(`http://ip-api.com/json/${ip}`, { timeout: 8000 });\n if (r.data.status === 'success') return ok(res, { ip: r.data.query, country: r.data.country, country_code: r.data.countryCode, region: r.data.regionName, city: r.data.city, latitude: r.data.lat, longitude: r.data.lon, timezone: r.data.timezone, isp: r.data.isp });\n } catch (_) {}\n return err(res, 'IP lookup failed. Try again.');\n }\n\n // ── URL SHORTENER ────────────────────────────────────────────────────────\n if (path === '/url/shorten') {\n const url = req.body?.url || req.query?.url;\n const alias = req.body?.alias || req.query?.alias || Math.random().toString(36).slice(2, 8);\n if (!url) return err(res, 'Missing: url', 400);\n try { new URL(url); } catch { return err(res, 'Invalid URL format. Must start with https://', 400); }\n try {\n const db = await getDB();\n const ref = db.collection('short_urls').doc(alias);\n const snap = await ref.get();\n if (!snap.exists) await ref.set({ url, alias, clicks: 0, created_at: new Date() });\n return ok(res, { original: url, short: `https://api-blacknode-zw.vercel.app/api/s/${alias}`, alias, clicks: snap.exists ? snap.data().clicks : 0 });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── NEWS ─────────────────────────────────────────────────────────────────\n if (path === '/news') {\n const { q, category = 'general', limit = '10' } = req.query;\n const ndKey = process.env.NEWSDATA_API_KEY;\n if (ndKey) {\n try {\n const params = { apikey: ndKey, language: 'en', size: Math.min(parseInt(limit), 50) };\n if (q) params.q = q; else params.category = category;\n const r = await axios.get('https://newsdata.io/api/1/news', { params, timeout: 12000 });\n if (r.data.status === 'success') return ok(res, { query: q || category, articles: (r.data.results || []).map(a => ({ title: a.title || '', description: a.description || '', url: a.link || '', image: a.image_url || '', source: a.source_id || '', published_at: a.pubDate || '' })) });\n } catch (_) {}\n }\n // RSS fallback\n const RSS = { general: 'https://feeds.bbci.co.uk/news/rss.xml', technology: 'https://feeds.bbci.co.uk/news/technology/rss.xml', sports: 'https://feeds.bbci.co.uk/sport/rss.xml', business: 'https://feeds.bbci.co.uk/news/business/rss.xml', health: 'https://feeds.bbci.co.uk/news/health/rss.xml', science: 'https://feeds.bbci.co.uk/news/science_and_environment/rss.xml' };\n try {\n const r = await axios.get(RSS[category] || RSS.general, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 12000 });\n const items = [];\n for (const m of r.data.matchAll(/<item>([\\s\\S]*?)<\\/item>/g)) {\n const i = m[1];\n const title = (i.match(/<title><!\\[CDATA\\[(.+?)\\]\\]>/) || i.match(/<title>(.+?)<\\/title>/))?.[1] || '';\n const link = i.match(/<link>(.+?)<\\/link>/)?.[1] || '';\n const desc = (i.match(/<description><!\\[CDATA\\[(.+?)\\]\\]>/) || i.match(/<description>(.+?)<\\/description>/))?.[1] || '';\n const pubDate = i.match(/<pubDate>(.+?)<\\/pubDate>/)?.[1] || '';\n if (title) items.push({ title: title.trim(), description: desc.replace(/<[^>]+>/g, '').slice(0, 200), url: link.trim(), source: 'BBC News', published_at: pubDate });\n if (items.length >= parseInt(limit)) break;\n }\n return ok(res, { query: q || category, total: items.length, articles: items, note: 'Set NEWSDATA_API_KEY for 200+ news sources' });\n } catch (e) { return err(res, 'News fetch failed. Try again.'); }\n }\n\n // ── DICTIONARY ───────────────────────────────────────────────────────────\n if (path === '/dictionary') {\n const { word, lang = 'en' } = req.query;\n if (!word) return err(res, 'Missing: word', 400);\n try {\n const r = await axios.get(`https://api.dictionaryapi.dev/api/v2/entries/${lang}/${encodeURIComponent(word.trim().toLowerCase())}`, { timeout: 10000 });\n const entry = r.data[0];\n return ok(res, { word: entry.word, phonetic: entry.phonetic || '', phonetics: (entry.phonetics || []).filter(p => p.text || p.audio).map(p => ({ text: p.text || '', audio: p.audio || '' })), meanings: (entry.meanings || []).map(m => ({ part_of_speech: m.partOfSpeech, definitions: (m.definitions || []).slice(0, 3).map(d => ({ definition: d.definition, example: d.example || '' })), synonyms: (m.synonyms || []).slice(0, 8), antonyms: (m.antonyms || []).slice(0, 5) })) });\n } catch (e) {\n if (e.response?.status === 404) return err(res, `No definition found for: \"${word}\"`, 404);\n return err(res, e.message);\n }\n }\n\n // ── GITHUB ───────────────────────────────────────────────────────────────\n if (path === '/github/user') {\n const { username, action = 'user', repo } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const headers = process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 'User-Agent': 'BlackNodeZW/1.0' } : { 'User-Agent': 'BlackNodeZW/1.0' };\n try {\n if (action === 'repos') {\n const r = await axios.get(`https://api.github.com/users/${username}/repos?sort=stars&per_page=20`, { headers, timeout: 10000 });\n return ok(res, { username, repos: r.data.map(r => ({ name: r.name, description: r.description || '', url: r.html_url, language: r.language || '', stars: r.stargazers_count, forks: r.forks_count })) });\n }\n if (action === 'repo' && repo) {\n const r = await axios.get(`https://api.github.com/repos/${username}/${repo}`, { headers, timeout: 10000 });\n return ok(res, { name: r.data.name, description: r.data.description || '', url: r.data.html_url, language: r.data.language || '', stars: r.data.stargazers_count, forks: r.data.forks_count, issues: r.data.open_issues_count });\n }\n const r = await axios.get(`https://api.github.com/users/${username}`, { headers, timeout: 10000 });\n const u = r.data;\n return ok(res, { username: u.login, name: u.name || '', bio: u.bio || '', avatar: u.avatar_url, url: u.html_url, followers: u.followers, following: u.following, public_repos: u.public_repos, location: u.location || '', company: u.company || '', twitter: u.twitter_username || '' });\n } catch (e) { return err(res, e.response?.data?.message || e.message); }\n }\n\n // ── SCREENSHOT ───────────────────────────────────────────────────────────\n if (path === '/screenshot') {\n const { url, width = '1280', height = '720', format = 'jpg' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n try { new URL(url); } catch { return err(res, 'Invalid URL. Must start with https://', 400); }\n const w = Math.min(parseInt(width) || 1280, 1920);\n const h = Math.min(parseInt(height) || 720, 1080);\n // Try screenshotone\n const ssKey = process.env.SCREENSHOTONE_API_KEY;\n if (ssKey) {\n try {\n const r = await axios.get('https://api.screenshotone.com/take', { params: { access_key: ssKey, url, viewport_width: w, viewport_height: h, format: 'jpg', block_ads: true }, responseType: 'arraybuffer', timeout: 30000 });\n res.setHeader('Content-Type', 'image/jpeg');\n return res.send(Buffer.from(r.data));\n } catch (_) {}\n }\n // Fallback: thum.io (free)\n try {\n const r = await axios.get(`https://image.thum.io/get/width/${w}/crop/${h}/${encodeURIComponent(url)}`, { responseType: 'arraybuffer', timeout: 25000, headers: { 'User-Agent': 'BlackNodeZW/1.0' } });\n if (r.data?.byteLength > 1000) {\n res.setHeader('Content-Type', 'image/jpeg');\n return res.send(Buffer.from(r.data));\n }\n } catch (_) {}\n // Fallback: return JSON with screenshot URL\n return ok(res, { url, screenshot_url: `https://image.thum.io/get/width/${w}/crop/${h}/${encodeURIComponent(url)}`, note: 'Open screenshot_url in browser to view the screenshot' });\n }\n\n // ── FUN ───────────────────────────────────────────────────────────────────\n if (path === '/fun') {\n const { type = 'quote', category = 'general' } = req.query;\n try {\n if (type === 'quote') { const r = await axios.get('https://zenquotes.io/api/random', { timeout: 8000 }); return ok(res, { type: 'quote', content: r.data[0].q, author: r.data[0].a }); }\n if (type === 'joke') { const r = await axios.get('https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,racist,sexist', { timeout: 8000 }); return ok(res, { type: 'joke', category: r.data.category, joke: r.data.joke || null, setup: r.data.setup || null, delivery: r.data.delivery || null }); }\n if (type === 'fact') { const r = await axios.get('https://uselessfacts.jsph.pl/api/v2/facts/random?language=en', { timeout: 8000 }); return ok(res, { type: 'fact', content: r.data.text }); }\n if (type === 'trivia') { const r = await axios.get('https://opentdb.com/api.php?amount=1&type=multiple', { timeout: 8000 }); const q = r.data.results?.[0]; if (q) return ok(res, { type: 'trivia', category: q.category, difficulty: q.difficulty, question: q.question, correct_answer: q.correct_answer, incorrect_answers: q.incorrect_answers }); }\n if (type === 'riddle') { const r = await axios.get('https://riddles-api.vercel.app/random', { timeout: 8000 }); return ok(res, { type: 'riddle', riddle: r.data.riddle, answer: r.data.answer }); }\n return err(res, `Invalid type: ${type}. Use: quote, joke, fact, trivia, riddle`, 400);\n } catch (e) { return err(res, e.message); }\n }\n\n // ── SPOTIFY ──────────────────────────────────────────────────────────────\n if (path === '/spotify/track') {\n const { url, action, q } = req.query;\n const clientId = process.env.SPOTIFY_CLIENT_ID, clientSecret = process.env.SPOTIFY_CLIENT_SECRET;\n if (!clientId || !clientSecret) return err(res, 'Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in Vercel environment variables. Free at developer.spotify.com', 400);\n try {\n const tokenR = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({ grant_type: 'client_credentials' }), { headers: { 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 });\n const headers = { 'Authorization': `Bearer ${tokenR.data.access_token}` };\n if (action === 'search' && q) {\n const r = await axios.get('https://api.spotify.com/v1/search', { params: { q, type: 'track', limit: 10, market: 'US' }, headers, timeout: 10000 });\n return ok(res, { query: q, tracks: (r.data.tracks?.items || []).map(t => ({ id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', preview_url: t.preview_url || '', spotify_url: t.external_urls?.spotify || '' })) });\n }\n if (!url) return err(res, 'Missing: url or use action=search&q=query', 400);\n const trackId = url.match(/track\\/([a-zA-Z0-9]+)/)?.[1];\n if (!trackId) return err(res, 'Invalid Spotify URL', 400);\n const r = await axios.get(`https://api.spotify.com/v1/tracks/${trackId}`, { headers, timeout: 10000 });\n const t = r.data;\n return ok(res, { id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', duration_ms: t.duration_ms, preview_url: t.preview_url || '', popularity: t.popularity, spotify_url: t.external_urls?.spotify || '' });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── MOVIE APIs (NEW) ─────────────────────────────────────────────────────\n if (path === '/movie/search') {\n const { q, year, type = 'movie', page = '1' } = req.query;\n if (!q) return err(res, 'Missing: q (movie title to search)', 400);\n const omdbKey = process.env.OMDB_API_KEY || 'trilogy'; // free demo key\n try {\n const params = { apikey: omdbKey, s: q, type, page };\n if (year) params.y = year;\n const r = await axios.get('https://www.omdbapi.com/', { params, timeout: 10000 });\n if (r.data.Response === 'False') return err(res, r.data.Error || 'No results found.', 404);\n return ok(res, { query: q, total: parseInt(r.data.totalResults) || 0, page: parseInt(page), results: (r.data.Search || []).map(m => ({ imdb_id: m.imdbID, title: m.Title, year: m.Year, type: m.Type, poster: m.Poster !== 'N/A' ? m.Poster : null })) });\n } catch (e) { return err(res, 'Movie search failed. Set OMDB_API_KEY (free at omdbapi.com) for reliable results.'); }\n }\n\n if (path === '/movie/details') {\n const { id, title } = req.query;\n if (!id && !title) return err(res, 'Missing: id (IMDb ID like tt1234567) or title', 400);\n const omdbKey = process.env.OMDB_API_KEY || 'trilogy';\n try {\n const params = { apikey: omdbKey, plot: 'full' };\n if (id) params.i = id; else params.t = title;\n const r = await axios.get('https://www.omdbapi.com/', { params, timeout: 10000 });\n if (r.data.Response === 'False') return err(res, r.data.Error || 'Movie not found.', 404);\n const m = r.data;\n return ok(res, { imdb_id: m.imdbID, title: m.Title, year: m.Year, rated: m.Rated, released: m.Released, runtime: m.Runtime, genre: m.Genre, director: m.Director, actors: m.Actors, plot: m.Plot, language: m.Language, country: m.Country, awards: m.Awards, poster: m.Poster !== 'N/A' ? m.Poster : null, imdb_rating: m.imdbRating, imdb_votes: m.imdbVotes, type: m.Type, box_office: m.BoxOffice || null });\n } catch (e) { return err(res, 'Movie details fetch failed. Set OMDB_API_KEY (free at omdbapi.com).'); }\n }\n\n if (path === '/movie/trending') {\n // Uses TMDB API for trending movies\n const tmdbKey = process.env.TMDB_API_KEY;\n if (!tmdbKey) return err(res, 'Set TMDB_API_KEY in Vercel environment variables. Free at themoviedb.org/settings/api', 400);\n const { time_window = 'week' } = req.query;\n try {\n const r = await axios.get(`https://api.themoviedb.org/3/trending/movie/${time_window}`, { params: { api_key: tmdbKey }, timeout: 10000 });\n return ok(res, { time_window, results: (r.data.results || []).map(m => ({ id: m.id, title: m.title, overview: m.overview?.slice(0, 200), release_date: m.release_date, rating: m.vote_average, poster: m.poster_path ? `https://image.tmdb.org/t/p/w500${m.poster_path}` : null, popularity: m.popularity })) });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── WHATSAPP BOT API (NEW) ───────────────────────────────────────────────\n if (path === '/whatsapp/send') {\n const { phone, message } = req.body || {};\n if (!phone || !message) return err(res, 'Missing: phone (with country code) and message', 400);\n const waKey = process.env.WHATSAPP_API_KEY;\n const waPhone = process.env.WHATSAPP_PHONE_ID;\n if (!waKey || !waPhone) return err(res, 'Set WHATSAPP_API_KEY and WHATSAPP_PHONE_ID in Vercel environment variables. Get from developers.facebook.com/docs/whatsapp', 400);\n try {\n const r = await axios.post(`https://graph.facebook.com/v18.0/${waPhone}/messages`, { messaging_product: 'whatsapp', to: phone.replace(/\\D/g, ''), type: 'text', text: { body: message } }, { headers: { 'Authorization': `Bearer ${waKey}`, 'Content-Type': 'application/json' }, timeout: 15000 });\n return ok(res, { success: true, message_id: r.data.messages?.[0]?.id, to: phone, status: 'sent' });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n if (path === '/whatsapp/verify') {\n // WhatsApp webhook verification\n const mode = req.query['hub.mode'];\n const token = req.query['hub.verify_token'];\n const challenge = req.query['hub.challenge'];\n const verifyToken = process.env.WHATSAPP_VERIFY_TOKEN || 'blacknodezw';\n if (mode === 'subscribe' && token === verifyToken) return res.status(200).send(challenge);\n return err(res, 'Verification failed', 403);\n }\n\n // ── TELEGRAM BOT API (NEW) ───────────────────────────────────────────────\n if (path === '/telegram/send') {\n const { chat_id, message, parse_mode = 'HTML' } = req.body || {};\n if (!chat_id || !message) return err(res, 'Missing: chat_id and message', 400);\n const botToken = process.env.TELEGRAM_BOT_TOKEN;\n if (!botToken) return err(res, 'Set TELEGRAM_BOT_TOKEN in Vercel environment variables. Get from @BotFather on Telegram.', 400);\n try {\n const r = await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, { chat_id, text: message, parse_mode }, { timeout: 15000 });\n return ok(res, { success: true, message_id: r.data.result?.message_id, chat_id, status: 'sent' });\n } catch (e) { return err(res, e.response?.data?.description || e.message); }\n }\n\n if (path === '/telegram/file') {\n const { chat_id, file_url, caption = '' } = req.body || {};\n if (!chat_id || !file_url) return err(res, 'Missing: chat_id and file_url', 400);\n const botToken = process.env.TELEGRAM_BOT_TOKEN;\n if (!botToken) return err(res, 'Set TELEGRAM_BOT_TOKEN in Vercel environment variables.', 400);\n try {\n const ext = file_url.split('.').pop().split('?')[0].toLowerCase();\n let endpoint = 'sendDocument', key = 'document';\n if (['jpg','jpeg','png','webp'].includes(ext)) { endpoint = 'sendPhoto'; key = 'photo'; }\n else if (['mp4','mov','avi'].includes(ext)) { endpoint = 'sendVideo'; key = 'video'; }\n else if (['mp3','ogg','wav'].includes(ext)) { endpoint = 'sendAudio'; key = 'audio'; }\n const r = await axios.post(`https://api.telegram.org/bot${botToken}/${endpoint}`, { chat_id, [key]: file_url, caption }, { timeout: 15000 });\n return ok(res, { success: true, message_id: r.data.result?.message_id, status: 'sent' });\n } catch (e) { return err(res, e.response?.data?.description || e.message); }\n }\n\n // ── CLOUD STORAGE (NEW) ─────────────────────────────────────────────────\n if (path === '/storage/upload') {\n const { base64, filename, folder = 'uploads' } = req.body || {};\n if (!base64 || !filename) return err(res, 'Missing: base64 (file data) and filename', 400);\n const cloudName = process.env.CLOUDINARY_CLOUD_NAME;\n const apiKey = process.env.CLOUDINARY_API_KEY;\n const apiSecret = process.env.CLOUDINARY_API_SECRET;\n if (!cloudName || !apiKey || !apiSecret) return err(res, 'Set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY and CLOUDINARY_API_SECRET. Free at cloudinary.com', 400);\n try {\n const timestamp = Math.floor(Date.now() / 1000);\n const crypto = require('crypto');\n const sig = crypto.createHash('sha1').update(`folder=${folder}&public_id=${filename}×tamp=${timestamp}${apiSecret}`).digest('hex');\n const FormData = require('form-data');\n const form = new FormData();\n form.append('file', base64);\n form.append('public_id', filename);\n form.append('folder', folder);\n form.append('timestamp', timestamp);\n form.append('api_key', apiKey);\n form.append('signature', sig);\n const r = await axios.post(`https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`, form, { headers: form.getHeaders(), timeout: 60000 });\n return ok(res, { success: true, url: r.data.secure_url, public_id: r.data.public_id, format: r.data.format, size: r.data.bytes, width: r.data.width || null, height: r.data.height || null });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n if (path === '/storage/delete') {\n const { public_id } = req.body || {};\n if (!public_id) return err(res, 'Missing: public_id', 400);\n const cloudName = process.env.CLOUDINARY_CLOUD_NAME;\n const apiKey = process.env.CLOUDINARY_API_KEY;\n const apiSecret = process.env.CLOUDINARY_API_SECRET;\n if (!cloudName || !apiKey || !apiSecret) return err(res, 'Set CLOUDINARY credentials in Vercel environment variables.', 400);\n try {\n const timestamp = Math.floor(Date.now() / 1000);\n const crypto = require('crypto');\n const sig = crypto.createHash('sha1').update(`public_id=${public_id}×tamp=${timestamp}${apiSecret}`).digest('hex');\n const r = await axios.post(`https://api.cloudinary.com/v1_1/${cloudName}/image/destroy`, { public_id, timestamp, api_key: apiKey, signature: sig }, { timeout: 15000 });\n return ok(res, { success: r.data.result === 'ok', public_id });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── PHONE LOOKUP (NEW) ───────────────────────────────────────────────────\n if (path === '/phone/lookup') {\n const { number } = req.query;\n if (!number) return err(res, 'Missing: number (include country code e.g. +263771234567)', 400);\n try {\n const r = await axios.get(`https://phonevalidation.abstractapi.com/v1/?api_key=${process.env.ABSTRACTAPI_KEY || 'demo'}&phone=${number.replace(/\\s/g, '')}`, { timeout: 10000 });\n return ok(res, { number: r.data.phone, valid: r.data.valid, country: r.data.country?.name, country_code: r.data.country?.calling_code, location: r.data.location, carrier: r.data.carrier, line_type: r.data.line_type, international_format: r.data.format?.international });\n } catch (e) { return err(res, 'Phone lookup requires ABSTRACTAPI_KEY. Free at abstractapi.com'); }\n }\n\n // ── EMAIL VALIDATOR (NEW) ────────────────────────────────────────────────\n if (path === '/email/validate') {\n const { email } = req.query;\n if (!email) return err(res, 'Missing: email', 400);\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n const valid = emailRegex.test(email);\n const domain = email.split('@')[1] || '';\n const disposableDomains = ['mailinator.com','tempmail.com','guerrillamail.com','10minutemail.com','throwaway.email','temp-mail.org'];\n const isDisposable = disposableDomains.includes(domain.toLowerCase());\n return ok(res, { email, valid, format_valid: valid, domain, is_disposable: isDisposable, is_free: ['gmail.com','yahoo.com','outlook.com','hotmail.com','icloud.com'].includes(domain.toLowerCase()) });\n }\n\n // ── PASTEBIN / TEXT STORAGE (NEW) ────────────────────────────────────────\n if (path === '/paste/create') {\n const { content, title = 'Untitled', expiry = 'never' } = req.body || {};\n if (!content) return err(res, 'Missing: content', 400);\n try {\n const db = await getDB();\n const id = Math.random().toString(36).slice(2, 10);\n const expiryMap = { '1h': 3600000, '24h': 86400000, '7d': 604800000, 'never': null };\n const expireAt = expiryMap[expiry] ? new Date(Date.now() + expiryMap[expiry]) : null;\n await db.collection('pastes').doc(id).set({ content, title, created_at: new Date(), expire_at: expireAt, views: 0 });\n return ok(res, { id, title, url: `https://api-blacknode-zw.vercel.app/api/paste/${id}`, expiry, created_at: new Date().toISOString() });\n } catch (e) { return err(res, e.message); }\n }\n\n const pasteMatch = path.match(/^\\/paste\\/(.+)$/);\n if (pasteMatch) {\n try {\n const db = await getDB();\n const snap = await db.collection('pastes').doc(pasteMatch[1]).get();\n if (!snap.exists) return err(res, 'Paste not found.', 404);\n const data = snap.data();\n if (data.expire_at && new Date() > data.expire_at.toDate()) { await snap.ref.delete(); return err(res, 'Paste has expired.', 404); }\n snap.ref.update({ views: (data.views || 0) + 1 }).catch(() => {});\n return ok(res, { id: pasteMatch[1], title: data.title, content: data.content, views: (data.views || 0) + 1, created_at: data.created_at?.toDate?.()?.toISOString() });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── RANDOM STUFF (NEW) ───────────────────────────────────────────────────\n if (path === '/random/password') {\n const { length = '16', numbers = 'true', symbols = 'true', uppercase = 'true' } = req.query;\n const len = Math.min(Math.max(parseInt(length) || 16, 8), 128);\n let chars = 'abcdefghijklmnopqrstuvwxyz';\n if (uppercase === 'true') chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\n if (numbers === 'true') chars += '0123456789';\n if (symbols === 'true') chars += '!@#$%^&*()-_=+[]{}|;:,.<>?';\n const password = Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');\n return ok(res, { password, length: len, strength: len >= 16 && numbers === 'true' && symbols === 'true' ? 'strong' : len >= 12 ? 'medium' : 'weak' });\n }\n\n if (path === '/random/uuid') {\n const { count = '1' } = req.query;\n const n = Math.min(parseInt(count) || 1, 50);\n const uuids = Array.from({ length: n }, () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }));\n return ok(res, { count: n, uuids: n === 1 ? uuids[0] : uuids });\n }\n\n if (path === '/random/color') {\n const hex = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');\n const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);\n return ok(res, { hex, rgb: `rgb(${r},${g},${b})`, r, g, b, hsl: rgbToHsl(r, g, b) });\n }\n\n// PATCH - add to index.js before the NOT FOUND section\n\n // ── INSTAGRAM DOWNLOAD FIX ───────────────────────────────────────────────\n // (already in file, replacing with better version)\n\n // ── FACEBOOK DOWNLOAD FIX ────────────────────────────────────────────────\n // Uses getfvid.com scrape + cobalt fallback\n\n // ── PINTEREST IMAGE SEARCH (NEW) ─────────────────────────────────────────\n if (path === '/pinterest/search') {\n const { q, count = '20' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n try {\n const r = await axios.get('https://www.pinterest.com/search/pins/', {\n params: { q, rs: 'typed' },\n headers: {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n 'Accept': 'text/html,application/xhtml+xml',\n 'Accept-Language': 'en-US,en;q=0.9'\n },\n timeout: 15000\n });\n const matches = [...r.data.matchAll(/\"images\":\\{\"236x\":\\{\"url\":\"([^\"]+)\"/g)];\n const hdMatches = [...r.data.matchAll(/\"orig\":\\{\"url\":\"([^\"]+)\"/g)];\n const titleMatches = [...r.data.matchAll(/\"grid_description\":\"([^\"]+)\"/g)];\n const results = matches.slice(0, parseInt(count)).map((m, i) => ({\n thumbnail: m[1].replace(/\\\\/g, ''),\n image: hdMatches[i]?.1?.replace(/\\\\/g, '') || m[1].replace(/\\\\/g, ''),\n title: titleMatches[i]?.[1] || q\n }));\n if (results.length) return ok(res, { query: q, total: results.length, images: results });\n // Fallback to Unsplash with pinterest style\n const imgs = [];\n for (let i = 0; i < Math.min(parseInt(count), 10); i++) {\n const seed = encodeURIComponent(q) + i;\n imgs.push({ image: `https://source.unsplash.com/800x800/?${seed}`, thumbnail: `https://source.unsplash.com/400x400/?${seed}`, title: q });\n }\n return ok(res, { query: q, total: imgs.length, images: imgs, note: 'Pinterest blocked direct scraping. Showing Unsplash results instead.' });\n } catch (e) {\n return err(res, 'Pinterest search failed. Try again.');\n }\n }\n\n // ── DEEPSEEK AI (NEW) ────────────────────────────────────────────────────\n if (path === '/ai/deepseek') {\n const { message, system = 'You are a helpful AI assistant.', model = 'deepseek-chat' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const dsKey = process.env.DEEPSEEK_API_KEY;\n if (!dsKey) return err(res, 'Set DEEPSEEK_API_KEY in Vercel environment variables. Get free key at platform.deepseek.com', 400);\n try {\n const r = await axios.post('https://api.deepseek.com/chat/completions',\n { model, messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 2048 },\n { headers: { 'Authorization': `Bearer ${dsKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: r.data.model, tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── CLAUDE AI (NEW) ──────────────────────────────────────────────────────\n if (path === '/ai/claude') {\n const { message, system = 'You are a helpful AI assistant.', model = 'claude-3-haiku-20240307' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const claudeKey = process.env.ANTHROPIC_API_KEY;\n if (!claudeKey) return err(res, 'Set ANTHROPIC_API_KEY in Vercel environment variables. Get key at console.anthropic.com', 400);\n try {\n const r = await axios.post('https://api.anthropic.com/v1/messages',\n { model, max_tokens: 1024, system, messages: [{ role: 'user', content: message }] },\n { headers: { 'x-api-key': claudeKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { message, response: r.data.content[0]?.text || '', model: r.data.model, tokens_used: (r.data.usage?.input_tokens || 0) + (r.data.usage?.output_tokens || 0) });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── GROK AI (NEW) ────────────────────────────────────────────────────────\n if (path === '/ai/grok') {\n const { message, system = 'You are Grok, a helpful AI assistant.', model = 'grok-beta' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const grokKey = process.env.GROK_API_KEY || process.env.XAI_API_KEY;\n if (!grokKey) return err(res, 'Set GROK_API_KEY in Vercel environment variables. Get key at console.x.ai', 400);\n try {\n const r = await axios.post('https://api.x.ai/v1/chat/completions',\n { model, messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 },\n { headers: { 'Authorization': `Bearer ${grokKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: r.data.model, tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── GEMINI AI (NEW) ──────────────────────────────────────────────────────\n if (path === '/ai/gemini') {\n const { message, model = 'gemini-1.5-flash' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_KEY;\n if (!geminiKey) return err(res, 'Set GEMINI_API_KEY in Vercel environment variables. Get free key at aistudio.google.com', 400);\n try {\n const r = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${geminiKey}`,\n { contents: [{ parts: [{ text: message }] }], generationConfig: { maxOutputTokens: 1024 } },\n { headers: { 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n const response = r.data.candidates?.[0]?.content?.parts?.[0]?.text || '';\n return ok(res, { message, response, model, tokens_used: r.data.usageMetadata?.totalTokenCount || 0 });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── CHATGPT / OPENAI GPT-4 (NEW explicit endpoint) ───────────────────────\n if (path === '/ai/chatgpt') {\n const { message, system = 'You are ChatGPT, a helpful AI assistant.', model = 'gpt-4o-mini' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (!openaiKey) return err(res, 'Set OPENAI_API_KEY in Vercel environment variables. Get key at platform.openai.com', 400);\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions',\n { model, messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 2048 },\n { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: r.data.model, tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n // ── AI VIDEO GENERATOR (NEW) ─────────────────────────────────────────────\n if (path === '/ai/video') {\n const prompt = req.body?.prompt || req.query?.prompt;\n const duration = parseInt(req.body?.duration || req.query?.duration || '5');\n if (!prompt) return err(res, 'Missing: prompt', 400);\n // Method 1: Stability AI video (needs key)\n const stabilityKey = process.env.STABILITY_API_KEY;\n if (stabilityKey) {\n try {\n const r = await axios.post('https://api.stability.ai/v2beta/image-to-video',\n { image: `https://image.pollinations.ai/prompt/${encodeURIComponent(prompt)}?width=1024&height=576&nologo=true`, seed: 0, cfg_scale: 1.8, motion_bucket_id: 127 },\n { headers: { 'Authorization': `Bearer ${stabilityKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n if (r.data.id) {\n return ok(res, { prompt, generation_id: r.data.id, status: 'processing', check_url: `https://api-blacknode-zw.vercel.app/api/ai/video/result?id=${r.data.id}`, note: 'Video is being generated. Check check_url in 30-60 seconds.' });\n }\n } catch (_) {}\n }\n // Method 2: Luma AI Dream Machine (needs key)\n const lumaKey = process.env.LUMAAI_API_KEY;\n if (lumaKey) {\n try {\n const r = await axios.post('https://api.lumalabs.ai/dream-machine/v1/generations',\n { prompt, aspect_ratio: '16:9', loop: false },\n { headers: { 'Authorization': `Bearer ${lumaKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { prompt, generation_id: r.data.id, status: r.data.state, model: 'luma-dream-machine', note: 'Video generation started. Takes 1-3 minutes.' });\n } catch (_) {}\n }\n // Free fallback info\n return ok(res, {\n prompt, status: 'no_key',\n message: 'AI video generation requires an API key.',\n options: [\n { provider: 'Stability AI', key_name: 'STABILITY_API_KEY', signup: 'platform.stability.ai', free_credits: true },\n { provider: 'Luma AI', key_name: 'LUMAAI_API_KEY', signup: 'lumalabs.ai/dream-machine', free_credits: true },\n { provider: 'RunwayML', key_name: 'RUNWAY_API_KEY', signup: 'runwayml.com', free_credits: true }\n ],\n note: 'Set any of these keys in Vercel environment variables.'\n });\n }\n\n // ── AI VIDEO RESULT CHECK ────────────────────────────────────────────────\n if (path === '/ai/video/result') {\n const { id } = req.query;\n if (!id) return err(res, 'Missing: id', 400);\n const stabilityKey = process.env.STABILITY_API_KEY;\n if (stabilityKey) {\n try {\n const r = await axios.get(`https://api.stability.ai/v2beta/image-to-video/result/${id}`,\n { headers: { 'Authorization': `Bearer ${stabilityKey}`, Accept: 'video/*' }, responseType: 'arraybuffer', timeout: 30000 }\n );\n if (r.status === 200) {\n const b64 = Buffer.from(r.data).toString('base64');\n return ok(res, { id, status: 'complete', video_base64: `data:video/mp4;base64,${b64}` });\n }\n return ok(res, { id, status: 'processing', note: 'Still generating. Try again in 10 seconds.' });\n } catch (_) {}\n }\n return err(res, 'Set STABILITY_API_KEY to check video generation status.');\n }\n\n // ── AI VISION / IMAGE ANALYSIS (NEW) ────────────────────────────────────\n if (path === '/ai/vision') {\n const { image_url, prompt = 'Describe this image in detail.' } = req.body || {};\n if (!image_url) return err(res, 'Missing: image_url', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions',\n { model: 'gpt-4o-mini', messages: [{ role: 'user', content: [{ type: 'text', text: prompt }, { type: 'image_url', image_url: { url: image_url } }] }], max_tokens: 1024 },\n { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { image_url, prompt, response: r.data.choices[0]?.message?.content || '', model: 'gpt-4o-mini' });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n const geminiKey = process.env.GEMINI_API_KEY;\n if (geminiKey) {\n try {\n const r = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${geminiKey}`,\n { contents: [{ parts: [{ text: prompt }, { inline_data: { mime_type: 'image/jpeg', data: image_url } }] }] },\n { headers: { 'Content-Type': 'application/json' }, timeout: 30000 }\n );\n return ok(res, { image_url, prompt, response: r.data.candidates?.[0]?.content?.parts?.[0]?.text || '', model: 'gemini-1.5-flash' });\n } catch (_) {}\n }\n return err(res, 'Set OPENAI_API_KEY or GEMINI_API_KEY for image analysis.');\n }\n\n // ── AI SUMMARIZER (NEW) ──────────────────────────────────────────────────\n if (path === '/ai/summarize') {\n const { text, url: pageUrl, length = 'medium' } = req.body || {};\n if (!text && !pageUrl) return err(res, 'Missing: text or url', 400);\n let content = text;\n if (pageUrl && !text) {\n try {\n const r = await axios.get(pageUrl, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 12000 });\n const cheerio = require('cheerio');\n const $ = cheerio.load(r.data);\n $('script,style,nav,footer,header,aside').remove();\n content = $('body').text().replace(/\\s+/g, ' ').trim().slice(0, 8000);\n } catch (e) { return err(res, 'Could not fetch URL content: ' + e.message); }\n }\n const maxWords = { short: 100, medium: 250, long: 500 }[length] || 250;\n const prompt = `Summarize the following text in about ${maxWords} words:\\n\\n${content}`;\n const openaiKey = process.env.OPENAI_API_KEY;\n const groqKey = process.env.GROQ_API_KEY;\n try {\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: prompt }], max_tokens: 600 }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 25000 });\n return ok(res, { summary: r.data.choices[0]?.message?.content || '', length, model: 'gpt-3.5-turbo' });\n }\n if (groqKey) {\n const r = await axios.post('https://api.groq.com/openai/v1/chat/completions', { model: 'llama3-8b-8192', messages: [{ role: 'user', content: prompt }], max_tokens: 600 }, { headers: { 'Authorization': `Bearer ${groqKey}`, 'Content-Type': 'application/json' }, timeout: 20000 });\n return ok(res, { summary: r.data.choices[0]?.message?.content || '', length, model: 'llama3-8b' });\n }\n const r = await axios.get(`https://text.pollinations.ai/${encodeURIComponent(prompt)}`, { timeout: 25000 });\n return ok(res, { summary: typeof r.data === 'string' ? r.data : JSON.stringify(r.data), length, model: 'pollinations' });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── COUNTRY INFO (NEW) ───────────────────────────────────────────────────\n if (path === '/country') {\n const { name, code } = req.query;\n if (!name && !code) return err(res, 'Missing: name or code', 400);\n try {\n const query = code ? `alpha/${code}` : `name/${encodeURIComponent(name)}`;\n const r = await axios.get(`https://restcountries.com/v3.1/${query}`, { timeout: 10000 });\n const c = Array.isArray(r.data) ? r.data[0] : r.data;\n return ok(res, { name: c.name?.common, official_name: c.name?.official, capital: c.capital?.[0] || '', region: c.region, subregion: c.subregion, population: c.population, area_km2: c.area, languages: Object.values(c.languages || {}), currencies: Object.values(c.currencies || {}).map(cu => ({ name: cu.name, symbol: cu.symbol })), timezones: c.timezones, calling_codes: c.idd?.suffixes?.map(s => (c.idd?.root || '') + s) || [], flag_emoji: c.flag, flag_url: c.flags?.png || '', continent: c.continents?.[0] || '' });\n } catch (e) { return err(res, `Country not found: ${name || code}`); }\n }\n\n\n\n // ── NOT FOUND ─────────────────────────────────────────────────────────────\n return res.status(404).json({ status: 404, error: `Endpoint not found: ${path}`, docs: 'https://api-blacknode-zw.vercel.app', available_endpoints: ['/tiktok/download','/tiktok/user','/tiktok/search','/tiktok/trending','/youtube/mp3','/youtube/mp4','/youtube/info','/youtube/channel','/instagram/download','/instagram/user','/facebook/download','/twitter/download','/pinterest/download','/spotify/track','/image/search','/image/generate','/image/removebg','/ai/chat','/ai/generate','/ai/tts','/ai/translate','/ai/music','/weather','/currency/convert','/qrcode/generate','/ip/lookup','/url/shorten','/news','/dictionary','/github/user','/screenshot','/fun','/movie/search','/movie/details','/movie/trending','/whatsapp/send','/telegram/send','/telegram/file','/storage/upload','/phone/lookup','/email/validate','/paste/create','/random/password','/random/uuid','/random/color'] });\n\n } catch (e) {\n return err(res, e.message);\n }\n};\n\nfunction rgbToHsl(r, g, b) {\n r /= 255; g /= 255; b /= 255;\n const max = Math.max(r, g, b), min = Math.min(r, g, b);\n let h, s, l = (max + min) / 2;\n if (max === min) { h = s = 0; } else {\n const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; }\n h /= 6;\n }\n return `hsl(${Math.round(h * 360)},${Math.round(s * 100)}%,${Math.round(l * 100)}%)`;\n}\n"}, "54b3d86418bed17e54181b5bf775ce5f2ade26bc": {"t": 1785767099.7195866, "n": 63259, "h": "07817c9f450ca1fb", "c": "// api/index.js - Black Node ZW - All endpoints unified\nconst axios = require('axios');\nconst QRCode = require('qrcode');\n\nfunction cors(res) {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type,x-api-key,Authorization');\n}\nconst ok = (res, data) => res.status(200).json({ status: 200, data });\nconst err = (res, msg, code = 500) => res.status(code).json({ status: code, error: msg });\n\n// ── FIREBASE ──────────────────────────────────────────────────────────────────\nlet _db = null;\nasync function getDB() {\n if (_db) return _db;\n const { initializeApp, getApps, cert } = require('firebase-admin/app');\n const { getFirestore } = require('firebase-admin/firestore');\n if (!getApps().length) {\n if (process.env.FIREBASE_SERVICE_ACCOUNT) {\n initializeApp({ credential: cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT)) });\n } else { initializeApp({ projectId: 'apiblacknodezw' }); }\n }\n _db = getFirestore();\n return _db;\n}\n\nasync function authCheck(req, res) {\n if (req.method === 'OPTIONS') { res.status(200).end(); return false; }\n const key = req.headers['x-api-key'] || req.query.api_key || req.query.apikey || req.query.key;\n if (!key) { err(res, 'Missing API key. Add x-api-key header or ?api_key=YOUR_KEY to URL.', 401); return false; }\n try {\n const db = await getDB();\n const snap = await db.collection('users').where('apiKey', '==', key).limit(1).get();\n if (snap.empty) { err(res, 'Invalid API key.', 403); return false; }\n const user = snap.docs[0].data();\n const limit = user.plan === 'pro' ? 50000 : 1000;\n if ((user.requests || 0) >= limit) { err(res, 'Monthly limit reached. Upgrade to Pro.', 429); return false; }\n snap.docs[0].ref.update({ requests: (user.requests || 0) + 1 }).catch(() => {});\n return true;\n } catch (e) { return true; }\n}\n\n// ── HELPERS ───────────────────────────────────────────────────────────────────\n// Extract YouTube video ID from any YouTube URL including Shorts\nfunction getYTId(url) {\n return url.match(/(?:v=|youtu\\.be\\/|embed\\/|shorts\\/|\\/v\\/)([^&\\n?#]+)/)?.[1] || null;\n}\n\n// Cobalt download helper with retries\nasync function cobaltDownload(url, opts = {}) {\n try {\n const resp = await axios.post('https://api.cobalt.tools/api/json',\n { url, ...opts },\n { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, timeout: 25000 }\n );\n return resp.data;\n } catch (e) {\n return null;\n }\n}\n\n// ── MAIN ROUTER ───────────────────────────────────────────────────────────────\nmodule.exports = async (req, res) => {\n cors(res);\n if (req.method === 'OPTIONS') return res.status(200).end();\n\n const path = req.url.replace(/\\?.*/, '').replace(/^\\/api/, '');\n\n // No auth routes\n if (path === '' || path === '/' || path === '/status') {\n return res.json({ status: 'online', name: 'Black Node ZW API', version: '1.0.0', base_url: 'https://api-blacknode-zw.vercel.app', endpoints: 40, timestamp: new Date().toISOString() });\n }\n\n // Short URL redirect\n const shortMatch = path.match(/^\\/s\\/(.+)$/);\n if (shortMatch) {\n try {\n const db = await getDB();\n const snap = await db.collection('short_urls').doc(shortMatch[1]).get();\n if (!snap.exists) return res.status(404).send('Link not found');\n snap.ref.update({ clicks: (snap.data().clicks || 0) + 1 }).catch(() => {});\n return res.redirect(301, snap.data().url);\n } catch (e) { return res.redirect('https://api-blacknode-zw.vercel.app'); }\n }\n\n if (!(await authCheck(req, res))) return;\n\n try {\n\n // ── TIKTOK ──────────────────────────────────────────────────────────────\n if (path === '/tiktok/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const decodedUrl = decodeURIComponent(url);\n try {\n const r = await axios.post('https://www.tikwm.com/api/',\n new URLSearchParams({ url: decodedUrl, hd: '1' }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 }\n );\n const d = r.data?.data;\n if (r.data?.code === 0 && d) {\n return ok(res, { title: d.title || '', author: '@' + (d.author?.unique_id || ''), author_name: d.author?.nickname || '', thumbnail: d.cover || '', download_url_hd: d.hdplay || d.play || '', download_url_sd: d.play || '', music_url: d.music || '', duration: d.duration || 0, play_count: d.play_count || 0, like_count: d.digg_count || 0 });\n }\n } catch (e) {}\n // Fallback: cobalt\n const cd = await cobaltDownload(decodedUrl);\n if (cd?.status === 'stream' || cd?.status === 'redirect') {\n return ok(res, { title: 'TikTok Video', download_url_hd: cd.url, download_url_sd: cd.url });\n }\n return err(res, 'Could not download. Make sure the video is public and the URL is correct.');\n }\n\n if (path === '/tiktok/user') {\n const { username, include_videos = 'false', video_count = '10' } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n const r = await axios.post('https://www.tikwm.com/api/user/info',\n new URLSearchParams({ unique_id: clean }),\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 }\n );\n if (r.data?.code !== 0) return err(res, 'User not found or account is private.');\n const u = r.data.data?.user || {}, s = r.data.data?.stats || {};\n const result = { username: u.uniqueId || clean, nickname: u.nickname || '', bio: u.signature || '', avatar: u.avatarLarger || '', verified: u.verified || false, private: u.privateAccount || false, followers: s.followerCount || 0, following: s.followingCount || 0, likes: s.heartCount || 0, video_count: s.videoCount || 0, profile_url: `https://www.tiktok.com/@${clean}` };\n if (include_videos === 'true') {\n try {\n const vr = await axios.post('https://www.tikwm.com/api/user/posts', new URLSearchParams({ unique_id: clean, count: Math.min(parseInt(video_count), 30), cursor: '0' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (vr.data?.code === 0) result.recent_videos = (vr.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0 }));\n } catch (_) {}\n }\n return ok(res, result);\n }\n\n if (path === '/tiktok/search') {\n const { q, count = '20', cursor = '0' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const r = await axios.post('https://www.tikwm.com/api/feed/search', new URLSearchParams({ keywords: q, count: Math.min(parseInt(count), 50), cursor }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 });\n if (r.data?.code !== 0) return err(res, 'Search failed. Try again.');\n return ok(res, { query: q, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' } })) });\n }\n\n if (path === '/tiktok/trending') {\n const { region = 'US', count = '20' } = req.query;\n const r = await axios.post('https://www.tikwm.com/api/feed/list', new URLSearchParams({ region, count: Math.min(parseInt(count), 50) }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 20000 });\n if (r.data?.code !== 0) return err(res, 'Could not fetch trending.');\n return ok(res, { region, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', play_count: v.play_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' } })) });\n }\n\n // ── YOUTUBE ──────────────────────────────────────────────────────────────\n if (path === '/youtube/mp3' || path === '/youtube/mp4') {\n const { url, quality = '720' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = getYTId(url);\n if (!videoId) return err(res, 'Invalid YouTube URL. Supported: youtube.com/watch, youtu.be, youtube.com/shorts', 400);\n const fullUrl = `https://www.youtube.com/watch?v=${videoId}`;\n const isAudio = path === '/youtube/mp3';\n\n // Get title\n let title = 'YouTube Video', channel = 'YouTube';\n try {\n const oe = await axios.get(`https://www.youtube.com/oembed?url=${encodeURIComponent(fullUrl)}&format=json`, { timeout: 6000 });\n title = oe.data.title || title;\n channel = oe.data.author_name || channel;\n } catch (_) {}\n\n const thumbnail = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;\n\n // Try cobalt first\n const cd = await cobaltDownload(fullUrl, isAudio ? { isAudioOnly: true, aFormat: 'mp3', audioBitrate: '320' } : { vQuality: quality });\n if (cd?.status === 'stream' || cd?.status === 'redirect') {\n return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.url, quality: isAudio ? '320kbps' : quality + 'p', format: isAudio ? 'mp3' : 'mp4' });\n }\n if (cd?.status === 'picker' && cd.picker?.length) {\n return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.picker[0].url, quality: quality + 'p', format: 'mp4' });\n }\n\n // Fallback: yt-dlp via loader.to free API\n try {\n const loaderResp = await axios.get(`https://loader.to/api/button/?url=${encodeURIComponent(fullUrl)}&f=${isAudio ? 'mp3' : 'mp4-' + quality}`, { timeout: 15000, headers: { 'User-Agent': 'Mozilla/5.0' } });\n if (loaderResp.data) {\n return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: `https://loader.to/api/download/?url=${encodeURIComponent(fullUrl)}&f=${isAudio ? 'mp3' : 'mp4'}`, format: isAudio ? 'mp3' : 'mp4', note: 'Use download_url in browser to download' });\n }\n } catch (_) {}\n\n return err(res, 'Could not fetch. cobalt.tools may be temporarily down. Try again in a moment.');\n }\n\n if (path === '/youtube/info') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = getYTId(url);\n if (!videoId) return err(res, 'Invalid YouTube URL', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (ytKey) {\n try {\n const r = await axios.get('https://www.googleapis.com/youtube/v3/videos', { params: { key: ytKey, id: videoId, part: 'snippet,statistics' }, timeout: 10000 });\n const item = r.data.items?.[0];\n if (item) return ok(res, { video_id: videoId, title: item.snippet.title, channel: item.snippet.channelTitle, thumbnail: item.snippet.thumbnails?.high?.url || '', views: parseInt(item.statistics.viewCount) || 0, likes: parseInt(item.statistics.likeCount) || 0, comments: parseInt(item.statistics.commentCount) || 0, published_at: item.snippet.publishedAt });\n } catch (_) {}\n }\n const r = await axios.get(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`, { timeout: 8000 });\n return ok(res, { video_id: videoId, title: r.data.title, channel: r.data.author_name, thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`, url: `https://www.youtube.com/watch?v=${videoId}` });\n }\n\n if (path === '/youtube/channel') {\n const { channel } = req.query;\n if (!channel) return err(res, 'Missing: channel', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (!ytKey) return err(res, 'Set YOUTUBE_API_KEY in Vercel environment variables for this endpoint.', 400);\n const search = await axios.get('https://www.googleapis.com/youtube/v3/search', { params: { key: ytKey, q: channel.replace(/^@/, ''), type: 'channel', part: 'id', maxResults: 1 }, timeout: 10000 });\n const channelId = search.data.items?.[0]?.id?.channelId;\n if (!channelId) return err(res, 'Channel not found.', 404);\n const stats = await axios.get('https://www.googleapis.com/youtube/v3/channels', { params: { key: ytKey, id: channelId, part: 'snippet,statistics' }, timeout: 10000 });\n const ch = stats.data.items?.[0];\n if (!ch) return err(res, 'Channel not found.', 404);\n return ok(res, { channel_id: channelId, title: ch.snippet.title, description: ch.snippet.description?.slice(0, 300), avatar: ch.snippet.thumbnails?.high?.url || '', subscribers: parseInt(ch.statistics.subscriberCount) || 0, total_views: parseInt(ch.statistics.viewCount) || 0, video_count: parseInt(ch.statistics.videoCount) || 0 });\n }\n\n // ── INSTAGRAM ────────────────────────────────────────────────────────────\n if (path === '/instagram/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n // Method 1: cobalt\n const cd = await cobaltDownload(url);\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { type: 'video', download_url: cd.url });\n if (cd?.status === 'picker') return ok(res, { type: 'gallery', items: cd.picker.map(p => ({ type: p.type, url: p.url })) });\n // Method 2: instadownloader\n try {\n const r = await axios.get(`https://instagram-downloader-download-instagram-videos-stories.p.rapidapi.com/index?url=${encodeURIComponent(url)}`, {\n headers: { 'X-RapidAPI-Key': process.env.RAPIDAPI_KEY || '', 'X-RapidAPI-Host': 'instagram-downloader-download-instagram-videos-stories.p.rapidapi.com' },\n timeout: 15000\n });\n if (r.data?.media) return ok(res, { type: 'video', download_url: r.data.media });\n } catch (_) {}\n return err(res, 'Could not download. Make sure the post is public. Instagram frequently blocks scrapers.');\n }\n\n if (path === '/instagram/user') {\n const { username } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n // Method 1: Instagram API\n try {\n const r = await axios.get(`https://www.instagram.com/api/v1/users/web_profile_info/?username=${clean}`, {\n headers: { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15', 'X-IG-App-ID': '936619743392459', Referer: `https://www.instagram.com/${clean}/`, Accept: '*/*' },\n timeout: 15000\n });\n const u = r.data?.data?.user;\n if (u) return ok(res, { username: u.username, full_name: u.full_name || '', bio: u.biography || '', avatar: u.profile_pic_url_hd || u.profile_pic_url || '', verified: u.is_verified || false, private: u.is_private || false, followers: u.edge_followed_by?.count || 0, following: u.edge_follow?.count || 0, posts: u.edge_owner_to_timeline_media?.count || 0 });\n } catch (_) {}\n // Method 2: page scrape\n try {\n const r = await axios.get(`https://www.instagram.com/${clean}/`, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', Accept: 'text/html' }, timeout: 12000 });\n const desc = r.data.match(/<meta name=\"description\" content=\"([^\"]+)\"/)?.[1] || '';\n const fMatch = desc.match(/([\\d,.KMB]+)\\s*Followers/i);\n const fgMatch = desc.match(/([\\d,.KMB]+)\\s*Following/i);\n const pMatch = desc.match(/([\\d,.KMB]+)\\s*Posts/i);\n const parseK = s => { if (!s) return 0; s = s.replace(/,/g, '').toUpperCase(); if (s.includes('M')) return Math.round(parseFloat(s) * 1e6); if (s.includes('K')) return Math.round(parseFloat(s) * 1e3); return parseInt(s) || 0; };\n return ok(res, { username: clean, followers: fMatch ? parseK(fMatch[1]) : 0, following: fgMatch ? parseK(fgMatch[1]) : 0, posts: pMatch ? parseK(pMatch[1]) : 0, profile_url: `https://www.instagram.com/${clean}/`, note: 'Instagram limits profile data. Set RAPIDAPI_KEY for full data.' });\n } catch (e) {\n return err(res, 'Instagram profile fetch failed. Instagram actively blocks scrapers. Try again later.');\n }\n }\n\n // ── OTHER DOWNLOADERS ────────────────────────────────────────────────────\n if (path === '/facebook/download' || path === '/twitter/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const cd = await cobaltDownload(url);\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { download_url: cd.url, quality: 'hd' });\n if (cd?.status === 'picker' && cd.picker?.length) return ok(res, { items: cd.picker.map(p => ({ quality: p.quality || 'unknown', url: p.url })) });\n return err(res, 'Could not download. Make sure the content is public.');\n }\n\n if (path === '/pinterest/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n try {\n const cheerio = require('cheerio');\n const r = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0', Accept: 'text/html' }, maxRedirects: 5, timeout: 15000 });\n const $ = cheerio.load(r.data);\n const videoUrl = $('meta[property=\"og:video\"]').attr('content') || '';\n const imageUrl = $('meta[property=\"og:image\"]').attr('content') || '';\n if (!videoUrl && !imageUrl) return err(res, 'No media found. Make sure the pin is public.');\n const hdImage = imageUrl.replace(/\\/\\d+x\\//, '/originals/');\n return ok(res, { type: videoUrl ? 'video' : 'image', title: $('meta[property=\"og:title\"]').attr('content') || '', download_url: videoUrl || hdImage, thumbnail: imageUrl, hd_image: hdImage });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── IMAGE ────────────────────────────────────────────────────────────────\n if (path === '/image/search') {\n const { q, count = '10' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const gKey = process.env.GOOGLE_SEARCH_API_KEY, cx = process.env.GOOGLE_SEARCH_CX;\n if (gKey && cx) {\n try {\n const r = await axios.get('https://www.googleapis.com/customsearch/v1', { params: { key: gKey, cx, q, searchType: 'image', num: Math.min(parseInt(count), 10) }, timeout: 10000 });\n return ok(res, { query: q, images: (r.data.items || []).map(i => ({ url: i.link, thumbnail: i.image?.thumbnailLink || '', title: i.title, source: i.image?.contextLink || '' })) });\n } catch (_) {}\n }\n // Fallback: Unsplash public API (no key needed)\n try {\n const r = await axios.get(`https://source.unsplash.com/featured/?${encodeURIComponent(q)}`, { maxRedirects: 0, timeout: 8000, validateStatus: s => s < 400 });\n const finalUrl = r.headers?.location || r.request?.res?.responseUrl || '';\n if (finalUrl) return ok(res, { query: q, images: [{ url: finalUrl, thumbnail: finalUrl, title: q, source: 'unsplash.com' }], note: 'Set GOOGLE_SEARCH_API_KEY for more results' });\n } catch (_) {}\n // Fallback: Pixabay free API\n try {\n const r = await axios.get('https://pixabay.com/api/', { params: { key: process.env.PIXABAY_API_KEY || '44556677-abc123def456', q, image_type: 'photo', per_page: Math.min(parseInt(count), 20) }, timeout: 10000 });\n if (r.data?.hits?.length) return ok(res, { query: q, images: r.data.hits.map(i => ({ url: i.largeImageURL || i.webformatURL, thumbnail: i.previewURL, title: i.tags, source: 'pixabay.com' })) });\n } catch (_) {}\n return err(res, 'Image search failed. Set GOOGLE_SEARCH_API_KEY and GOOGLE_SEARCH_CX in Vercel environment variables for reliable image search.');\n }\n\n if (path === '/image/generate') {\n const prompt = req.body?.prompt || req.query?.prompt;\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const style = req.body?.style || req.query?.style || 'realistic';\n const styleMap = { anime: 'anime style,', art: 'digital art,', cartoon: 'cartoon,', sketch: 'pencil sketch,' };\n const fullPrompt = ((styleMap[style] || '') + ' ' + prompt).trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/images/generations', { model: 'dall-e-3', prompt: fullPrompt, n: 1, size: '1024x1024' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 60000 });\n return ok(res, { prompt: fullPrompt, image_url: r.data.data[0]?.url, model: 'dall-e-3' });\n } catch (_) {}\n }\n const seed = Math.floor(Math.random() * 1000000);\n const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(fullPrompt)}?width=1024&height=1024&model=flux&seed=${seed}&nologo=true`;\n return ok(res, { prompt: fullPrompt, image_url: imageUrl, model: 'flux', seed, note: 'Open image_url in browser to view the generated image' });\n }\n\n if (path === '/image/removebg') {\n const imageUrl = req.body?.url || req.query?.url;\n if (!imageUrl) return err(res, 'Missing: url', 400);\n const rbKey = process.env.REMOVEBG_API_KEY;\n if (rbKey) {\n try {\n const FormData = require('form-data');\n const form = new FormData();\n form.append('image_url', imageUrl);\n form.append('size', 'auto');\n const r = await axios.post('https://api.remove.bg/v1.0/removebg', form, { headers: { ...form.getHeaders(), 'X-Api-Key': rbKey }, responseType: 'arraybuffer', timeout: 30000 });\n const b64 = Buffer.from(r.data).toString('base64');\n return ok(res, { original: imageUrl, result_base64: `data:image/png;base64,${b64}`, format: 'png', provider: 'remove.bg' });\n } catch (_) {}\n }\n // Free fallback: bg.removal.ai\n try {\n const r = await axios.post('https://api.photoroom.com/v1/segment', { image_url: imageUrl }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });\n if (r.data?.image_url) return ok(res, { original: imageUrl, result_url: r.data.image_url, format: 'png', provider: 'photoroom' });\n } catch (_) {}\n return err(res, 'Background removal requires REMOVEBG_API_KEY. Get a free key at remove.bg/api');\n }\n\n // ── AI ───────────────────────────────────────────────────────────────────\n if (path === '/ai/chat') {\n const { message, system = 'You are a helpful AI assistant.', model = 'default' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 25000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'gpt-3.5-turbo', tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (_) {}\n }\n // Free fallback: Groq (very fast free AI)\n const groqKey = process.env.GROQ_API_KEY;\n if (groqKey) {\n try {\n const r = await axios.post('https://api.groq.com/openai/v1/chat/completions', { model: 'llama3-8b-8192', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Authorization': `Bearer ${groqKey}`, 'Content-Type': 'application/json' }, timeout: 20000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'llama3-8b (groq)', tokens_used: r.data.usage?.total_tokens || 0 });\n } catch (_) {}\n }\n // Final fallback: DeepInfra\n try {\n const r = await axios.post('https://api.deepinfra.com/v1/openai/chat/completions', { model: 'meta-llama/Meta-Llama-3-8B-Instruct', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'llama-3-8b' });\n } catch (e) { return err(res, 'AI service temporarily unavailable. Set OPENAI_API_KEY or GROQ_API_KEY (free at groq.com) for reliable AI chat.'); }\n }\n\n if (path === '/ai/generate') {\n const { prompt, type = 'default', length = 'medium', tone = 'professional' } = req.body || {};\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const typeMap = { article: 'Write a detailed article about:', code: 'Write clean well-commented code for:', story: 'Write a creative short story about:', email: 'Write a professional email for:', essay: 'Write an essay about:', caption: 'Write social media captions for:' };\n const maxTokens = { short: 300, medium: 700, long: 1400 }[length] || 700;\n const fullPrompt = `${typeMap[type] || ''} ${prompt}. Tone: ${tone}`.trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: fullPrompt }], max_tokens: maxTokens }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 });\n const output = r.data.choices[0]?.message?.content || '';\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length });\n } catch (_) {}\n }\n // Fallback: Pollinations text\n try {\n const r = await axios.get(`https://text.pollinations.ai/${encodeURIComponent(fullPrompt)}`, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 30000 });\n const output = typeof r.data === 'string' ? r.data : JSON.stringify(r.data);\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length, model: 'pollinations' });\n } catch (e) { return err(res, 'AI generation timed out. Set OPENAI_API_KEY or GROQ_API_KEY for faster responses.'); }\n }\n\n if (path === '/ai/tts') {\n const text = req.body?.text || req.query?.text;\n const voice = req.body?.voice || req.query?.voice || 'Brian';\n if (!text) return err(res, 'Missing: text', 400);\n if (text.length > 3000) return err(res, 'Text too long. Max 3000 characters.', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n try {\n const r = await axios.post('https://api.openai.com/v1/audio/speech', { model: 'tts-1', input: text, voice: 'alloy' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, responseType: 'arraybuffer', timeout: 25000 });\n res.setHeader('Content-Type', 'audio/mpeg');\n return res.send(Buffer.from(r.data));\n } catch (_) {}\n }\n const voices = ['Brian','Amy','Emma','Matthew','Joanna','Joey','Kendra','Kimberly','Salli','Justin'];\n const sel = voices.includes(voice) ? voice : 'Brian';\n const audioUrl = `https://api.streamelements.com/kappa/v2/speech?voice=${sel}&text=${encodeURIComponent(text)}`;\n return ok(res, { text, voice: sel, audio_url: audioUrl, format: 'mp3', note: 'Open audio_url in browser or use in an audio player' });\n }\n\n if (path === '/ai/translate') {\n const { text, to, from = 'auto' } = req.query;\n if (!text) return err(res, 'Missing: text', 400);\n if (!to) return err(res, 'Missing: to (target language code e.g. es, fr, sn)', 400);\n try {\n const r = await axios.get('https://translate.googleapis.com/translate_a/single', { params: { client: 'gtx', sl: from === 'auto' ? 'auto' : from, tl: to, dt: 't', q: text }, headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data[0]?.map(s => s[0]).join('') || '', from: r.data[2] || from, to });\n } catch (_) {\n try {\n const r = await axios.get('https://api.mymemory.translated.net/get', { params: { q: text, langpair: `${from}|${to}` }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data.responseData.translatedText, from, to });\n } catch (e) { return err(res, 'Translation failed. Try again.'); }\n }\n }\n\n // NEW: AI Music Generator\n if (path === '/ai/music') {\n const prompt = req.body?.prompt || req.query?.prompt;\n const style = req.body?.style || req.query?.style || 'pop';\n if (!prompt) return err(res, 'Missing: prompt', 400);\n // Uses MusicGen via Hugging Face\n const hfKey = process.env.HUGGINGFACE_API_KEY;\n if (hfKey) {\n try {\n const r = await axios.post('https://api-inference.huggingface.co/models/facebook/musicgen-small',\n { inputs: `${style} music: ${prompt}` },\n { headers: { 'Authorization': `Bearer ${hfKey}`, 'Content-Type': 'application/json' }, responseType: 'arraybuffer', timeout: 60000 }\n );\n const b64 = Buffer.from(r.data).toString('base64');\n return ok(res, { prompt, style, audio_base64: `data:audio/wav;base64,${b64}`, format: 'wav', model: 'musicgen-small', note: 'Set HUGGINGFACE_API_KEY for this endpoint' });\n } catch (_) {}\n }\n // Fallback: Suno AI public URL\n return ok(res, { prompt, style, note: 'Set HUGGINGFACE_API_KEY for AI music generation. Free at huggingface.co', alternative: 'Try suno.com or udio.com for free AI music generation' });\n }\n\n // ── WEATHER ──────────────────────────────────────────────────────────────\n if (path === '/weather') {\n const { city, units = 'metric' } = req.query;\n if (!city) return err(res, 'Missing: city', 400);\n try {\n const geo = await axios.get('https://nominatim.openstreetmap.org/search', { params: { q: city, format: 'json', limit: 1 }, headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 10000 });\n if (!geo.data?.length) return err(res, `City not found: \"${city}\"`, 404);\n const { lat, lon, display_name } = geo.data[0];\n const w = await axios.get('https://api.open-meteo.com/v1/forecast', { params: { latitude: lat, longitude: lon, current: 'temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,precipitation', daily: 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum', temperature_unit: units === 'imperial' ? 'fahrenheit' : 'celsius', wind_speed_unit: units === 'imperial' ? 'mph' : 'kmh', timezone: 'auto', forecast_days: 7 }, timeout: 12000 });\n const c = w.data.current, unit = units === 'imperial' ? '°F' : '°C';\n const WMO = { 0:'Clear sky',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Foggy',51:'Light drizzle',61:'Slight rain',63:'Moderate rain',65:'Heavy rain',71:'Slight snow',80:'Showers',95:'Thunderstorm' };\n return ok(res, { city: display_name.split(',')[0], full_location: display_name, coordinates: { lat: parseFloat(lat), lon: parseFloat(lon) }, temperature: `${c.temperature_2m}${unit}`, feels_like: `${c.apparent_temperature}${unit}`, condition: WMO[c.weather_code] || 'Unknown', humidity: `${c.relative_humidity_2m}%`, wind_speed: `${c.wind_speed_10m} ${units === 'imperial' ? 'mph' : 'km/h'}`, precipitation: `${c.precipitation}mm`, units, forecast: w.data.daily.time.map((d, i) => ({ date: d, condition: WMO[w.data.daily.weather_code[i]] || 'Unknown', high: `${w.data.daily.temperature_2m_max[i]}${unit}`, low: `${w.data.daily.temperature_2m_min[i]}${unit}`, precipitation: `${w.data.daily.precipitation_sum[i]}mm` })) });\n } catch (e) { return err(res, `Weather fetch failed: ${e.message}`); }\n }\n\n // ── CURRENCY ─────────────────────────────────────────────────────────────\n if (path === '/currency/convert') {\n let { from = 'USD', to, amount = '1', action } = req.query;\n // Clean up common user mistakes\n from = from.replace(/[^A-Za-z]/g, '').toUpperCase().slice(0, 3);\n if (!from || from.length < 2) from = 'USD';\n try {\n const r = await axios.get(`https://open.er-api.com/v6/latest/${from}`, { timeout: 10000 });\n if (r.data.result !== 'success') throw new Error('Exchange rate API failed');\n if (action === 'rates') return ok(res, { base: from, rates: r.data.rates, last_updated: r.data.time_last_update_utc });\n if (!to) return err(res, 'Missing: to (target currency code e.g. ZAR, EUR, GBP)', 400);\n to = to.replace(/[^A-Za-z]/g, '').toUpperCase().slice(0, 3);\n const rate = r.data.rates[to];\n if (!rate) return err(res, `Currency not found: \"${to}\". Use standard codes like USD, EUR, ZAR, GBP.`, 400);\n const amt = parseFloat(amount) || 1;\n return ok(res, { from, to, amount: amt, rate, result: parseFloat((amt * rate).toFixed(6)), formatted: `${(amt * rate).toFixed(2)} ${to}`, last_updated: r.data.time_last_update_utc });\n } catch (e) {\n // Fallback: Frankfurter\n try {\n const to2 = (to || 'USD').replace(/[^A-Za-z]/g, '').toUpperCase().slice(0, 3);\n const r = await axios.get(`https://api.frankfurter.app/latest?from=${from}&to=${to2}`, { timeout: 10000 });\n const rate = r.data.rates[to2];\n if (!rate) return err(res, `Currency not found: \"${to2}\"`, 400);\n const amt = parseFloat(amount) || 1;\n return ok(res, { from, to: to2, amount: amt, rate, result: parseFloat((amt * rate).toFixed(6)), formatted: `${(amt * rate).toFixed(2)} ${to2}` });\n } catch (e2) { return err(res, 'Currency conversion failed. Try again.'); }\n }\n }\n\n // ── QR CODE ──────────────────────────────────────────────────────────────\n if (path === '/qrcode/generate') {\n const { data, size = '300', color = '000000', bg = 'ffffff', format = 'json' } = req.query;\n if (!data) return err(res, 'Missing: data', 400);\n const sizeNum = Math.min(Math.max(parseInt(size) || 300, 100), 1000);\n const opts = { width: sizeNum, margin: 2, color: { dark: `#${color.replace('#', '')}`, light: `#${bg.replace('#', '')}` } };\n // Always return base64/JSON by default so tester can display it\n if (format === 'svg') {\n const svg = await QRCode.toString(data, { ...opts, type: 'svg' });\n res.setHeader('Content-Type', 'image/svg+xml');\n return res.send(svg);\n }\n if (format === 'png') {\n res.setHeader('Content-Type', 'image/png');\n return res.send(await QRCode.toBuffer(data, opts));\n }\n // Default: return as JSON with base64\n const dataUrl = await QRCode.toDataURL(data, opts);\n return ok(res, { data, format: 'base64', size: `${sizeNum}x${sizeNum}`, image: dataUrl, note: 'Use format=png or format=svg in URL for direct image response' });\n }\n\n // ── IP LOOKUP ────────────────────────────────────────────────────────────\n if (path === '/ip/lookup') {\n let { ip } = req.query;\n if (!ip) ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || '';\n try {\n const r = await axios.get(`https://ipwho.is/${ip}`, { timeout: 8000 });\n if (r.data.success) return ok(res, { ip: r.data.ip, country: r.data.country, country_code: r.data.country_code, region: r.data.region, city: r.data.city, latitude: r.data.latitude, longitude: r.data.longitude, timezone: r.data.timezone?.id || '', isp: r.data.connection?.isp || '', org: r.data.connection?.org || '' });\n } catch (_) {}\n try {\n const r = await axios.get(`http://ip-api.com/json/${ip}`, { timeout: 8000 });\n if (r.data.status === 'success') return ok(res, { ip: r.data.query, country: r.data.country, country_code: r.data.countryCode, region: r.data.regionName, city: r.data.city, latitude: r.data.lat, longitude: r.data.lon, timezone: r.data.timezone, isp: r.data.isp });\n } catch (_) {}\n return err(res, 'IP lookup failed. Try again.');\n }\n\n // ── URL SHORTENER ────────────────────────────────────────────────────────\n if (path === '/url/shorten') {\n const url = req.body?.url || req.query?.url;\n const alias = req.body?.alias || req.query?.alias || Math.random().toString(36).slice(2, 8);\n if (!url) return err(res, 'Missing: url', 400);\n try { new URL(url); } catch { return err(res, 'Invalid URL format. Must start with https://', 400); }\n try {\n const db = await getDB();\n const ref = db.collection('short_urls').doc(alias);\n const snap = await ref.get();\n if (!snap.exists) await ref.set({ url, alias, clicks: 0, created_at: new Date() });\n return ok(res, { original: url, short: `https://api-blacknode-zw.vercel.app/api/s/${alias}`, alias, clicks: snap.exists ? snap.data().clicks : 0 });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── NEWS ─────────────────────────────────────────────────────────────────\n if (path === '/news') {\n const { q, category = 'general', limit = '10' } = req.query;\n const ndKey = process.env.NEWSDATA_API_KEY;\n if (ndKey) {\n try {\n const params = { apikey: ndKey, language: 'en', size: Math.min(parseInt(limit), 50) };\n if (q) params.q = q; else params.category = category;\n const r = await axios.get('https://newsdata.io/api/1/news', { params, timeout: 12000 });\n if (r.data.status === 'success') return ok(res, { query: q || category, articles: (r.data.results || []).map(a => ({ title: a.title || '', description: a.description || '', url: a.link || '', image: a.image_url || '', source: a.source_id || '', published_at: a.pubDate || '' })) });\n } catch (_) {}\n }\n // RSS fallback\n const RSS = { general: 'https://feeds.bbci.co.uk/news/rss.xml', technology: 'https://feeds.bbci.co.uk/news/technology/rss.xml', sports: 'https://feeds.bbci.co.uk/sport/rss.xml', business: 'https://feeds.bbci.co.uk/news/business/rss.xml', health: 'https://feeds.bbci.co.uk/news/health/rss.xml', science: 'https://feeds.bbci.co.uk/news/science_and_environment/rss.xml' };\n try {\n const r = await axios.get(RSS[category] || RSS.general, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 12000 });\n const items = [];\n for (const m of r.data.matchAll(/<item>([\\s\\S]*?)<\\/item>/g)) {\n const i = m[1];\n const title = (i.match(/<title><!\\[CDATA\\[(.+?)\\]\\]>/) || i.match(/<title>(.+?)<\\/title>/))?.[1] || '';\n const link = i.match(/<link>(.+?)<\\/link>/)?.[1] || '';\n const desc = (i.match(/<description><!\\[CDATA\\[(.+?)\\]\\]>/) || i.match(/<description>(.+?)<\\/description>/))?.[1] || '';\n const pubDate = i.match(/<pubDate>(.+?)<\\/pubDate>/)?.[1] || '';\n if (title) items.push({ title: title.trim(), description: desc.replace(/<[^>]+>/g, '').slice(0, 200), url: link.trim(), source: 'BBC News', published_at: pubDate });\n if (items.length >= parseInt(limit)) break;\n }\n return ok(res, { query: q || category, total: items.length, articles: items, note: 'Set NEWSDATA_API_KEY for 200+ news sources' });\n } catch (e) { return err(res, 'News fetch failed. Try again.'); }\n }\n\n // ── DICTIONARY ───────────────────────────────────────────────────────────\n if (path === '/dictionary') {\n const { word, lang = 'en' } = req.query;\n if (!word) return err(res, 'Missing: word', 400);\n try {\n const r = await axios.get(`https://api.dictionaryapi.dev/api/v2/entries/${lang}/${encodeURIComponent(word.trim().toLowerCase())}`, { timeout: 10000 });\n const entry = r.data[0];\n return ok(res, { word: entry.word, phonetic: entry.phonetic || '', phonetics: (entry.phonetics || []).filter(p => p.text || p.audio).map(p => ({ text: p.text || '', audio: p.audio || '' })), meanings: (entry.meanings || []).map(m => ({ part_of_speech: m.partOfSpeech, definitions: (m.definitions || []).slice(0, 3).map(d => ({ definition: d.definition, example: d.example || '' })), synonyms: (m.synonyms || []).slice(0, 8), antonyms: (m.antonyms || []).slice(0, 5) })) });\n } catch (e) {\n if (e.response?.status === 404) return err(res, `No definition found for: \"${word}\"`, 404);\n return err(res, e.message);\n }\n }\n\n // ── GITHUB ───────────────────────────────────────────────────────────────\n if (path === '/github/user') {\n const { username, action = 'user', repo } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const headers = process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 'User-Agent': 'BlackNodeZW/1.0' } : { 'User-Agent': 'BlackNodeZW/1.0' };\n try {\n if (action === 'repos') {\n const r = await axios.get(`https://api.github.com/users/${username}/repos?sort=stars&per_page=20`, { headers, timeout: 10000 });\n return ok(res, { username, repos: r.data.map(r => ({ name: r.name, description: r.description || '', url: r.html_url, language: r.language || '', stars: r.stargazers_count, forks: r.forks_count })) });\n }\n if (action === 'repo' && repo) {\n const r = await axios.get(`https://api.github.com/repos/${username}/${repo}`, { headers, timeout: 10000 });\n return ok(res, { name: r.data.name, description: r.data.description || '', url: r.data.html_url, language: r.data.language || '', stars: r.data.stargazers_count, forks: r.data.forks_count, issues: r.data.open_issues_count });\n }\n const r = await axios.get(`https://api.github.com/users/${username}`, { headers, timeout: 10000 });\n const u = r.data;\n return ok(res, { username: u.login, name: u.name || '', bio: u.bio || '', avatar: u.avatar_url, url: u.html_url, followers: u.followers, following: u.following, public_repos: u.public_repos, location: u.location || '', company: u.company || '', twitter: u.twitter_username || '' });\n } catch (e) { return err(res, e.response?.data?.message || e.message); }\n }\n\n // ── SCREENSHOT ───────────────────────────────────────────────────────────\n if (path === '/screenshot') {\n const { url, width = '1280', height = '720', format = 'jpg' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n try { new URL(url); } catch { return err(res, 'Invalid URL. Must start with https://', 400); }\n const w = Math.min(parseInt(width) || 1280, 1920);\n const h = Math.min(parseInt(height) || 720, 1080);\n // Try screenshotone\n const ssKey = process.env.SCREENSHOTONE_API_KEY;\n if (ssKey) {\n try {\n const r = await axios.get('https://api.screenshotone.com/take', { params: { access_key: ssKey, url, viewport_width: w, viewport_height: h, format: 'jpg', block_ads: true }, responseType: 'arraybuffer', timeout: 30000 });\n res.setHeader('Content-Type', 'image/jpeg');\n return res.send(Buffer.from(r.data));\n } catch (_) {}\n }\n // Fallback: thum.io (free)\n try {\n const r = await axios.get(`https://image.thum.io/get/width/${w}/crop/${h}/${encodeURIComponent(url)}`, { responseType: 'arraybuffer', timeout: 25000, headers: { 'User-Agent': 'BlackNodeZW/1.0' } });\n if (r.data?.byteLength > 1000) {\n res.setHeader('Content-Type', 'image/jpeg');\n return res.send(Buffer.from(r.data));\n }\n } catch (_) {}\n // Fallback: return JSON with screenshot URL\n return ok(res, { url, screenshot_url: `https://image.thum.io/get/width/${w}/crop/${h}/${encodeURIComponent(url)}`, note: 'Open screenshot_url in browser to view the screenshot' });\n }\n\n // ── FUN ───────────────────────────────────────────────────────────────────\n if (path === '/fun') {\n const { type = 'quote', category = 'general' } = req.query;\n try {\n if (type === 'quote') { const r = await axios.get('https://zenquotes.io/api/random', { timeout: 8000 }); return ok(res, { type: 'quote', content: r.data[0].q, author: r.data[0].a }); }\n if (type === 'joke') { const r = await axios.get('https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,racist,sexist', { timeout: 8000 }); return ok(res, { type: 'joke', category: r.data.category, joke: r.data.joke || null, setup: r.data.setup || null, delivery: r.data.delivery || null }); }\n if (type === 'fact') { const r = await axios.get('https://uselessfacts.jsph.pl/api/v2/facts/random?language=en', { timeout: 8000 }); return ok(res, { type: 'fact', content: r.data.text }); }\n if (type === 'trivia') { const r = await axios.get('https://opentdb.com/api.php?amount=1&type=multiple', { timeout: 8000 }); const q = r.data.results?.[0]; if (q) return ok(res, { type: 'trivia', category: q.category, difficulty: q.difficulty, question: q.question, correct_answer: q.correct_answer, incorrect_answers: q.incorrect_answers }); }\n if (type === 'riddle') { const r = await axios.get('https://riddles-api.vercel.app/random', { timeout: 8000 }); return ok(res, { type: 'riddle', riddle: r.data.riddle, answer: r.data.answer }); }\n return err(res, `Invalid type: ${type}. Use: quote, joke, fact, trivia, riddle`, 400);\n } catch (e) { return err(res, e.message); }\n }\n\n // ── SPOTIFY ──────────────────────────────────────────────────────────────\n if (path === '/spotify/track') {\n const { url, action, q } = req.query;\n const clientId = process.env.SPOTIFY_CLIENT_ID, clientSecret = process.env.SPOTIFY_CLIENT_SECRET;\n if (!clientId || !clientSecret) return err(res, 'Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in Vercel environment variables. Free at developer.spotify.com', 400);\n try {\n const tokenR = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({ grant_type: 'client_credentials' }), { headers: { 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 });\n const headers = { 'Authorization': `Bearer ${tokenR.data.access_token}` };\n if (action === 'search' && q) {\n const r = await axios.get('https://api.spotify.com/v1/search', { params: { q, type: 'track', limit: 10, market: 'US' }, headers, timeout: 10000 });\n return ok(res, { query: q, tracks: (r.data.tracks?.items || []).map(t => ({ id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', preview_url: t.preview_url || '', spotify_url: t.external_urls?.spotify || '' })) });\n }\n if (!url) return err(res, 'Missing: url or use action=search&q=query', 400);\n const trackId = url.match(/track\\/([a-zA-Z0-9]+)/)?.[1];\n if (!trackId) return err(res, 'Invalid Spotify URL', 400);\n const r = await axios.get(`https://api.spotify.com/v1/tracks/${trackId}`, { headers, timeout: 10000 });\n const t = r.data;\n return ok(res, { id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', duration_ms: t.duration_ms, preview_url: t.preview_url || '', popularity: t.popularity, spotify_url: t.external_urls?.spotify || '' });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── MOVIE APIs (NEW) ─────────────────────────────────────────────────────\n if (path === '/movie/search') {\n const { q, year, type = 'movie', page = '1' } = req.query;\n if (!q) return err(res, 'Missing: q (movie title to search)', 400);\n const omdbKey = process.env.OMDB_API_KEY || 'trilogy'; // free demo key\n try {\n const params = { apikey: omdbKey, s: q, type, page };\n if (year) params.y = year;\n const r = await axios.get('https://www.omdbapi.com/', { params, timeout: 10000 });\n if (r.data.Response === 'False') return err(res, r.data.Error || 'No results found.', 404);\n return ok(res, { query: q, total: parseInt(r.data.totalResults) || 0, page: parseInt(page), results: (r.data.Search || []).map(m => ({ imdb_id: m.imdbID, title: m.Title, year: m.Year, type: m.Type, poster: m.Poster !== 'N/A' ? m.Poster : null })) });\n } catch (e) { return err(res, 'Movie search failed. Set OMDB_API_KEY (free at omdbapi.com) for reliable results.'); }\n }\n\n if (path === '/movie/details') {\n const { id, title } = req.query;\n if (!id && !title) return err(res, 'Missing: id (IMDb ID like tt1234567) or title', 400);\n const omdbKey = process.env.OMDB_API_KEY || 'trilogy';\n try {\n const params = { apikey: omdbKey, plot: 'full' };\n if (id) params.i = id; else params.t = title;\n const r = await axios.get('https://www.omdbapi.com/', { params, timeout: 10000 });\n if (r.data.Response === 'False') return err(res, r.data.Error || 'Movie not found.', 404);\n const m = r.data;\n return ok(res, { imdb_id: m.imdbID, title: m.Title, year: m.Year, rated: m.Rated, released: m.Released, runtime: m.Runtime, genre: m.Genre, director: m.Director, actors: m.Actors, plot: m.Plot, language: m.Language, country: m.Country, awards: m.Awards, poster: m.Poster !== 'N/A' ? m.Poster : null, imdb_rating: m.imdbRating, imdb_votes: m.imdbVotes, type: m.Type, box_office: m.BoxOffice || null });\n } catch (e) { return err(res, 'Movie details fetch failed. Set OMDB_API_KEY (free at omdbapi.com).'); }\n }\n\n if (path === '/movie/trending') {\n // Uses TMDB API for trending movies\n const tmdbKey = process.env.TMDB_API_KEY;\n if (!tmdbKey) return err(res, 'Set TMDB_API_KEY in Vercel environment variables. Free at themoviedb.org/settings/api', 400);\n const { time_window = 'week' } = req.query;\n try {\n const r = await axios.get(`https://api.themoviedb.org/3/trending/movie/${time_window}`, { params: { api_key: tmdbKey }, timeout: 10000 });\n return ok(res, { time_window, results: (r.data.results || []).map(m => ({ id: m.id, title: m.title, overview: m.overview?.slice(0, 200), release_date: m.release_date, rating: m.vote_average, poster: m.poster_path ? `https://image.tmdb.org/t/p/w500${m.poster_path}` : null, popularity: m.popularity })) });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── WHATSAPP BOT API (NEW) ───────────────────────────────────────────────\n if (path === '/whatsapp/send') {\n const { phone, message } = req.body || {};\n if (!phone || !message) return err(res, 'Missing: phone (with country code) and message', 400);\n const waKey = process.env.WHATSAPP_API_KEY;\n const waPhone = process.env.WHATSAPP_PHONE_ID;\n if (!waKey || !waPhone) return err(res, 'Set WHATSAPP_API_KEY and WHATSAPP_PHONE_ID in Vercel environment variables. Get from developers.facebook.com/docs/whatsapp', 400);\n try {\n const r = await axios.post(`https://graph.facebook.com/v18.0/${waPhone}/messages`, { messaging_product: 'whatsapp', to: phone.replace(/\\D/g, ''), type: 'text', text: { body: message } }, { headers: { 'Authorization': `Bearer ${waKey}`, 'Content-Type': 'application/json' }, timeout: 15000 });\n return ok(res, { success: true, message_id: r.data.messages?.[0]?.id, to: phone, status: 'sent' });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n if (path === '/whatsapp/verify') {\n // WhatsApp webhook verification\n const mode = req.query['hub.mode'];\n const token = req.query['hub.verify_token'];\n const challenge = req.query['hub.challenge'];\n const verifyToken = process.env.WHATSAPP_VERIFY_TOKEN || 'blacknodezw';\n if (mode === 'subscribe' && token === verifyToken) return res.status(200).send(challenge);\n return err(res, 'Verification failed', 403);\n }\n\n // ── TELEGRAM BOT API (NEW) ───────────────────────────────────────────────\n if (path === '/telegram/send') {\n const { chat_id, message, parse_mode = 'HTML' } = req.body || {};\n if (!chat_id || !message) return err(res, 'Missing: chat_id and message', 400);\n const botToken = process.env.TELEGRAM_BOT_TOKEN;\n if (!botToken) return err(res, 'Set TELEGRAM_BOT_TOKEN in Vercel environment variables. Get from @BotFather on Telegram.', 400);\n try {\n const r = await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, { chat_id, text: message, parse_mode }, { timeout: 15000 });\n return ok(res, { success: true, message_id: r.data.result?.message_id, chat_id, status: 'sent' });\n } catch (e) { return err(res, e.response?.data?.description || e.message); }\n }\n\n if (path === '/telegram/file') {\n const { chat_id, file_url, caption = '' } = req.body || {};\n if (!chat_id || !file_url) return err(res, 'Missing: chat_id and file_url', 400);\n const botToken = process.env.TELEGRAM_BOT_TOKEN;\n if (!botToken) return err(res, 'Set TELEGRAM_BOT_TOKEN in Vercel environment variables.', 400);\n try {\n const ext = file_url.split('.').pop().split('?')[0].toLowerCase();\n let endpoint = 'sendDocument', key = 'document';\n if (['jpg','jpeg','png','webp'].includes(ext)) { endpoint = 'sendPhoto'; key = 'photo'; }\n else if (['mp4','mov','avi'].includes(ext)) { endpoint = 'sendVideo'; key = 'video'; }\n else if (['mp3','ogg','wav'].includes(ext)) { endpoint = 'sendAudio'; key = 'audio'; }\n const r = await axios.post(`https://api.telegram.org/bot${botToken}/${endpoint}`, { chat_id, [key]: file_url, caption }, { timeout: 15000 });\n return ok(res, { success: true, message_id: r.data.result?.message_id, status: 'sent' });\n } catch (e) { return err(res, e.response?.data?.description || e.message); }\n }\n\n // ── CLOUD STORAGE (NEW) ─────────────────────────────────────────────────\n if (path === '/storage/upload') {\n const { base64, filename, folder = 'uploads' } = req.body || {};\n if (!base64 || !filename) return err(res, 'Missing: base64 (file data) and filename', 400);\n const cloudName = process.env.CLOUDINARY_CLOUD_NAME;\n const apiKey = process.env.CLOUDINARY_API_KEY;\n const apiSecret = process.env.CLOUDINARY_API_SECRET;\n if (!cloudName || !apiKey || !apiSecret) return err(res, 'Set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY and CLOUDINARY_API_SECRET. Free at cloudinary.com', 400);\n try {\n const timestamp = Math.floor(Date.now() / 1000);\n const crypto = require('crypto');\n const sig = crypto.createHash('sha1').update(`folder=${folder}&public_id=${filename}×tamp=${timestamp}${apiSecret}`).digest('hex');\n const FormData = require('form-data');\n const form = new FormData();\n form.append('file', base64);\n form.append('public_id', filename);\n form.append('folder', folder);\n form.append('timestamp', timestamp);\n form.append('api_key', apiKey);\n form.append('signature', sig);\n const r = await axios.post(`https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`, form, { headers: form.getHeaders(), timeout: 60000 });\n return ok(res, { success: true, url: r.data.secure_url, public_id: r.data.public_id, format: r.data.format, size: r.data.bytes, width: r.data.width || null, height: r.data.height || null });\n } catch (e) { return err(res, e.response?.data?.error?.message || e.message); }\n }\n\n if (path === '/storage/delete') {\n const { public_id } = req.body || {};\n if (!public_id) return err(res, 'Missing: public_id', 400);\n const cloudName = process.env.CLOUDINARY_CLOUD_NAME;\n const apiKey = process.env.CLOUDINARY_API_KEY;\n const apiSecret = process.env.CLOUDINARY_API_SECRET;\n if (!cloudName || !apiKey || !apiSecret) return err(res, 'Set CLOUDINARY credentials in Vercel environment variables.', 400);\n try {\n const timestamp = Math.floor(Date.now() / 1000);\n const crypto = require('crypto');\n const sig = crypto.createHash('sha1').update(`public_id=${public_id}×tamp=${timestamp}${apiSecret}`).digest('hex');\n const r = await axios.post(`https://api.cloudinary.com/v1_1/${cloudName}/image/destroy`, { public_id, timestamp, api_key: apiKey, signature: sig }, { timeout: 15000 });\n return ok(res, { success: r.data.result === 'ok', public_id });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── PHONE LOOKUP (NEW) ───────────────────────────────────────────────────\n if (path === '/phone/lookup') {\n const { number } = req.query;\n if (!number) return err(res, 'Missing: number (include country code e.g. +263771234567)', 400);\n try {\n const r = await axios.get(`https://phonevalidation.abstractapi.com/v1/?api_key=${process.env.ABSTRACTAPI_KEY || 'demo'}&phone=${number.replace(/\\s/g, '')}`, { timeout: 10000 });\n return ok(res, { number: r.data.phone, valid: r.data.valid, country: r.data.country?.name, country_code: r.data.country?.calling_code, location: r.data.location, carrier: r.data.carrier, line_type: r.data.line_type, international_format: r.data.format?.international });\n } catch (e) { return err(res, 'Phone lookup requires ABSTRACTAPI_KEY. Free at abstractapi.com'); }\n }\n\n // ── EMAIL VALIDATOR (NEW) ────────────────────────────────────────────────\n if (path === '/email/validate') {\n const { email } = req.query;\n if (!email) return err(res, 'Missing: email', 400);\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n const valid = emailRegex.test(email);\n const domain = email.split('@')[1] || '';\n const disposableDomains = ['mailinator.com','tempmail.com','guerrillamail.com','10minutemail.com','throwaway.email','temp-mail.org'];\n const isDisposable = disposableDomains.includes(domain.toLowerCase());\n return ok(res, { email, valid, format_valid: valid, domain, is_disposable: isDisposable, is_free: ['gmail.com','yahoo.com','outlook.com','hotmail.com','icloud.com'].includes(domain.toLowerCase()) });\n }\n\n // ── PASTEBIN / TEXT STORAGE (NEW) ────────────────────────────────────────\n if (path === '/paste/create') {\n const { content, title = 'Untitled', expiry = 'never' } = req.body || {};\n if (!content) return err(res, 'Missing: content', 400);\n try {\n const db = await getDB();\n const id = Math.random().toString(36).slice(2, 10);\n const expiryMap = { '1h': 3600000, '24h': 86400000, '7d': 604800000, 'never': null };\n const expireAt = expiryMap[expiry] ? new Date(Date.now() + expiryMap[expiry]) : null;\n await db.collection('pastes').doc(id).set({ content, title, created_at: new Date(), expire_at: expireAt, views: 0 });\n return ok(res, { id, title, url: `https://api-blacknode-zw.vercel.app/api/paste/${id}`, expiry, created_at: new Date().toISOString() });\n } catch (e) { return err(res, e.message); }\n }\n\n const pasteMatch = path.match(/^\\/paste\\/(.+)$/);\n if (pasteMatch) {\n try {\n const db = await getDB();\n const snap = await db.collection('pastes').doc(pasteMatch[1]).get();\n if (!snap.exists) return err(res, 'Paste not found.', 404);\n const data = snap.data();\n if (data.expire_at && new Date() > data.expire_at.toDate()) { await snap.ref.delete(); return err(res, 'Paste has expired.', 404); }\n snap.ref.update({ views: (data.views || 0) + 1 }).catch(() => {});\n return ok(res, { id: pasteMatch[1], title: data.title, content: data.content, views: (data.views || 0) + 1, created_at: data.created_at?.toDate?.()?.toISOString() });\n } catch (e) { return err(res, e.message); }\n }\n\n // ── RANDOM STUFF (NEW) ───────────────────────────────────────────────────\n if (path === '/random/password') {\n const { length = '16', numbers = 'true', symbols = 'true', uppercase = 'true' } = req.query;\n const len = Math.min(Math.max(parseInt(length) || 16, 8), 128);\n let chars = 'abcdefghijklmnopqrstuvwxyz';\n if (uppercase === 'true') chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\n if (numbers === 'true') chars += '0123456789';\n if (symbols === 'true') chars += '!@#$%^&*()-_=+[]{}|;:,.<>?';\n const password = Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');\n return ok(res, { password, length: len, strength: len >= 16 && numbers === 'true' && symbols === 'true' ? 'strong' : len >= 12 ? 'medium' : 'weak' });\n }\n\n if (path === '/random/uuid') {\n const { count = '1' } = req.query;\n const n = Math.min(parseInt(count) || 1, 50);\n const uuids = Array.from({ length: n }, () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }));\n return ok(res, { count: n, uuids: n === 1 ? uuids[0] : uuids });\n }\n\n if (path === '/random/color') {\n const hex = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');\n const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);\n return ok(res, { hex, rgb: `rgb(${r},${g},${b})`, r, g, b, hsl: rgbToHsl(r, g, b) });\n }\n\n // ── NOT FOUND ─────────────────────────────────────────────────────────────\n return res.status(404).json({ status: 404, error: `Endpoint not found: ${path}`, docs: 'https://api-blacknode-zw.vercel.app', available_endpoints: ['/tiktok/download','/tiktok/user','/tiktok/search','/tiktok/trending','/youtube/mp3','/youtube/mp4','/youtube/info','/youtube/channel','/instagram/download','/instagram/user','/facebook/download','/twitter/download','/pinterest/download','/spotify/track','/image/search','/image/generate','/image/removebg','/ai/chat','/ai/generate','/ai/tts','/ai/translate','/ai/music','/weather','/currency/convert','/qrcode/generate','/ip/lookup','/url/shorten','/news','/dictionary','/github/user','/screenshot','/fun','/movie/search','/movie/details','/movie/trending','/whatsapp/send','/telegram/send','/telegram/file','/storage/upload','/phone/lookup','/email/validate','/paste/create','/random/password','/random/uuid','/random/color'] });\n\n } catch (e) {\n return err(res, e.message);\n }\n};\n\nfunction rgbToHsl(r, g, b) {\n r /= 255; g /= 255; b /= 255;\n const max = Math.max(r, g, b), min = Math.min(r, g, b);\n let h, s, l = (max + min) / 2;\n if (max === min) { h = s = 0; } else {\n const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; }\n h /= 6;\n }\n return `hsl(${Math.round(h * 360)},${Math.round(s * 100)}%,${Math.round(l * 100)}%)`;\n}\n"}, "be85a1b86a0e8609692b724711b9a6040a6cf9ef": {"t": 1785767103.4702337, "n": 33006, "h": "1318725c60a4f5d5", "c": "// api/index.js - Single unified handler for ALL endpoints\n// Keeps everything within Vercel free plan 12-function limit\n\nconst axios = require('axios');\nconst QRCode = require('qrcode');\n\nfunction cors(res) {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type,x-api-key,Authorization');\n}\nfunction ok(res, data) { return res.status(200).json({ status: 200, data }); }\nfunction err(res, msg, code = 500) { return res.status(code).json({ status: code, error: msg }); }\n\nlet _db = null;\nasync function getDB() {\n if (_db) return _db;\n const { initializeApp, getApps, cert } = require('firebase-admin/app');\n const { getFirestore } = require('firebase-admin/firestore');\n if (!getApps().length) {\n if (process.env.FIREBASE_SERVICE_ACCOUNT) {\n initializeApp({ credential: cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT)) });\n } else { initializeApp({ projectId: 'apiblacknodezw' }); }\n }\n _db = getFirestore();\n return _db;\n}\n\nasync function authCheck(req, res) {\n if (req.method === 'OPTIONS') { res.status(200).end(); return false; }\n const key = req.headers['x-api-key'];\n if (!key) { err(res, 'Missing API key in x-api-key header.', 401); return false; }\n try {\n const db = await getDB();\n const snap = await db.collection('users').where('apiKey', '==', key).limit(1).get();\n if (snap.empty) { err(res, 'Invalid API key.', 403); return false; }\n const user = snap.docs[0].data();\n const limit = user.plan === 'pro' ? 50000 : 1000;\n if ((user.requests || 0) >= limit) { err(res, 'Monthly limit reached. Upgrade to Pro.', 429); return false; }\n snap.docs[0].ref.update({ requests: (user.requests || 0) + 1 }).catch(() => {});\n return true;\n } catch (e) { return true; }\n}\n\nmodule.exports = async (req, res) => {\n cors(res);\n if (req.method === 'OPTIONS') return res.status(200).end();\n const path = req.url.replace(/\\?.*/, '').replace(/^\\/api/, '');\n\n // No auth routes\n if (path === '' || path === '/' || path === '/status') {\n return res.json({ status: 'online', name: 'Black Node ZW API', version: '1.0.0', base_url: 'https://api-blacknode-zw.vercel.app', endpoints: 30, timestamp: new Date().toISOString() });\n }\n const shortMatch = path.match(/^\\/s\\/(.+)$/);\n if (shortMatch) {\n try {\n const db = await getDB();\n const snap = await db.collection('short_urls').doc(shortMatch[1]).get();\n if (!snap.exists) return res.status(404).send('Link not found');\n snap.ref.update({ clicks: (snap.data().clicks || 0) + 1 }).catch(() => {});\n return res.redirect(301, snap.data().url);\n } catch (e) { return res.redirect('https://api-blacknode-zw.vercel.app'); }\n }\n\n if (!(await authCheck(req, res))) return;\n\n try {\n // TIKTOK\n if (path === '/tiktok/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const r = await axios.post('https://www.tikwm.com/api/', new URLSearchParams({ url, hd: '1' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n const d = r.data?.data;\n if (r.data?.code === 0 && d) return ok(res, { title: d.title || '', author: '@' + (d.author?.unique_id || ''), thumbnail: d.cover || '', download_url_hd: d.hdplay || d.play || '', download_url_sd: d.play || '', music_url: d.music || '', duration: d.duration || 0, play_count: d.play_count || 0, like_count: d.digg_count || 0 });\n return err(res, 'Could not fetch video. Check the URL.');\n }\n\n if (path === '/tiktok/user') {\n const { username, include_videos = 'false', video_count = '10' } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n const r = await axios.post('https://www.tikwm.com/api/user/info', new URLSearchParams({ unique_id: clean }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (r.data?.code !== 0) return err(res, 'User not found or account is private.');\n const u = r.data.data?.user || {}, s = r.data.data?.stats || {};\n const result = { username: u.uniqueId || clean, nickname: u.nickname || '', bio: u.signature || '', avatar: u.avatarLarger || '', verified: u.verified || false, private: u.privateAccount || false, followers: s.followerCount || 0, following: s.followingCount || 0, likes: s.heartCount || 0, video_count: s.videoCount || 0, profile_url: `https://www.tiktok.com/@${clean}` };\n if (include_videos === 'true') {\n try {\n const vr = await axios.post('https://www.tikwm.com/api/user/posts', new URLSearchParams({ unique_id: clean, count: Math.min(parseInt(video_count), 30), cursor: '0' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (vr.data?.code === 0) result.recent_videos = (vr.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0, url: `https://www.tiktok.com/@${clean}/video/${v.video_id}` }));\n } catch (_) {}\n }\n return ok(res, result);\n }\n\n if (path === '/tiktok/search') {\n const { q, count = '20', cursor = '0' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const r = await axios.post('https://www.tikwm.com/api/feed/search', new URLSearchParams({ keywords: q, count: Math.min(parseInt(count), 50), cursor }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (r.data?.code !== 0) return err(res, 'Search failed.');\n return ok(res, { query: q, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' }, url: `https://www.tiktok.com/@${v.author?.unique_id}/video/${v.video_id}` })) });\n }\n\n if (path === '/tiktok/trending') {\n const { region = 'US', count = '20' } = req.query;\n const r = await axios.post('https://www.tikwm.com/api/feed/list', new URLSearchParams({ region, count: Math.min(parseInt(count), 50) }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (r.data?.code !== 0) return err(res, 'Could not fetch trending.');\n return ok(res, { region, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' } })) });\n }\n\n // YOUTUBE\n if (path === '/youtube/mp3' || path === '/youtube/mp4') {\n const { url, quality = '720' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = url.match(/(?:v=|youtu\\.be\\/|embed\\/)([^&\\n?#]+)/)?.[1];\n if (!videoId) return err(res, 'Invalid YouTube URL', 400);\n const isAudio = path === '/youtube/mp3';\n const [cobalt, oembed] = await Promise.allSettled([\n axios.post('https://api.cobalt.tools/api/json', isAudio ? { url: `https://www.youtube.com/watch?v=${videoId}`, isAudioOnly: true, aFormat: 'mp3', audioBitrate: '320' } : { url: `https://www.youtube.com/watch?v=${videoId}`, vQuality: quality }, { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, timeout: 20000 }),\n axios.get(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`, { timeout: 6000 })\n ]);\n const title = oembed.value?.data?.title || 'YouTube';\n const channel = oembed.value?.data?.author_name || 'YouTube';\n const thumbnail = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;\n const cd = cobalt.value?.data;\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.url, quality: isAudio ? '320kbps' : quality + 'p', format: isAudio ? 'mp3' : 'mp4' });\n if (cd?.status === 'picker' && cd.picker?.length) return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.picker[0].url, quality: quality + 'p', format: 'mp4' });\n return err(res, 'Could not fetch. Try again.');\n }\n\n if (path === '/youtube/info') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = url.match(/(?:v=|youtu\\.be\\/|embed\\/)([^&\\n?#]+)/)?.[1];\n if (!videoId) return err(res, 'Invalid YouTube URL', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (ytKey) {\n const r = await axios.get('https://www.googleapis.com/youtube/v3/videos', { params: { key: ytKey, id: videoId, part: 'snippet,statistics' }, timeout: 10000 });\n const item = r.data.items?.[0];\n if (item) return ok(res, { video_id: videoId, title: item.snippet.title, channel: item.snippet.channelTitle, thumbnail: item.snippet.thumbnails?.high?.url || '', views: parseInt(item.statistics.viewCount) || 0, likes: parseInt(item.statistics.likeCount) || 0, comments: parseInt(item.statistics.commentCount) || 0, published_at: item.snippet.publishedAt });\n }\n const r = await axios.get(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`, { timeout: 8000 });\n return ok(res, { video_id: videoId, title: r.data.title, channel: r.data.author_name, thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg` });\n }\n\n if (path === '/youtube/channel') {\n const { channel } = req.query;\n if (!channel) return err(res, 'Missing: channel', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (!ytKey) return err(res, 'Set YOUTUBE_API_KEY in environment variables.');\n const search = await axios.get('https://www.googleapis.com/youtube/v3/search', { params: { key: ytKey, q: channel.replace(/^@/, ''), type: 'channel', part: 'id', maxResults: 1 }, timeout: 10000 });\n const channelId = search.data.items?.[0]?.id?.channelId;\n if (!channelId) return err(res, 'Channel not found.', 404);\n const stats = await axios.get('https://www.googleapis.com/youtube/v3/channels', { params: { key: ytKey, id: channelId, part: 'snippet,statistics' }, timeout: 10000 });\n const ch = stats.data.items?.[0];\n if (!ch) return err(res, 'Channel not found.', 404);\n return ok(res, { channel_id: channelId, title: ch.snippet.title, description: ch.snippet.description, avatar: ch.snippet.thumbnails?.high?.url || '', subscribers: parseInt(ch.statistics.subscriberCount) || 0, total_views: parseInt(ch.statistics.viewCount) || 0, video_count: parseInt(ch.statistics.videoCount) || 0 });\n }\n\n // INSTAGRAM\n if (path === '/instagram/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const r = await axios.post('https://api.cobalt.tools/api/json', { url }, { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, timeout: 20000 });\n if (r.data.status === 'stream' || r.data.status === 'redirect') return ok(res, { type: 'video', download_url: r.data.url });\n if (r.data.status === 'picker') return ok(res, { type: 'gallery', items: r.data.picker.map(p => ({ type: p.type, url: p.url })) });\n return err(res, 'Could not download. Make sure the post is public.');\n }\n\n if (path === '/instagram/user') {\n const { username } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n const r = await axios.get(`https://www.instagram.com/api/v1/users/web_profile_info/?username=${clean}`, { headers: { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)', 'X-IG-App-ID': '936619743392459', Referer: `https://www.instagram.com/${clean}/` }, timeout: 15000 });\n const u = r.data?.data?.user;\n if (!u) return err(res, 'User not found or account is private.', 404);\n return ok(res, { username: u.username, full_name: u.full_name || '', bio: u.biography || '', avatar: u.profile_pic_url_hd || u.profile_pic_url || '', verified: u.is_verified || false, private: u.is_private || false, followers: u.edge_followed_by?.count || 0, following: u.edge_follow?.count || 0, posts: u.edge_owner_to_timeline_media?.count || 0 });\n }\n\n // OTHER DOWNLOADERS\n if (path === '/facebook/download' || path === '/twitter/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const r = await axios.post('https://api.cobalt.tools/api/json', { url }, { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, timeout: 20000 });\n if (r.data.status === 'stream' || r.data.status === 'redirect') return ok(res, { download_url: r.data.url });\n if (r.data.status === 'picker') return ok(res, { items: r.data.picker.map(p => ({ type: p.type, url: p.url })) });\n return err(res, 'Could not download. Make sure the content is public.');\n }\n\n if (path === '/pinterest/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const cheerio = require('cheerio');\n const r = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0', Accept: 'text/html' }, maxRedirects: 5, timeout: 15000 });\n const $ = cheerio.load(r.data);\n const videoUrl = $('meta[property=\"og:video\"]').attr('content') || '';\n const imageUrl = $('meta[property=\"og:image\"]').attr('content') || '';\n if (!videoUrl && !imageUrl) return err(res, 'No media found. Make sure the pin is public.');\n const hdImage = imageUrl.replace(/\\/\\d+x\\//, '/originals/');\n return ok(res, { type: videoUrl ? 'video' : 'image', title: $('meta[property=\"og:title\"]').attr('content') || '', download_url: videoUrl || hdImage, thumbnail: imageUrl, hd_image: hdImage });\n }\n\n // IMAGE\n if (path === '/image/search') {\n const { q, count = '10' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const gKey = process.env.GOOGLE_SEARCH_API_KEY, cx = process.env.GOOGLE_SEARCH_CX;\n if (gKey && cx) {\n const r = await axios.get('https://www.googleapis.com/customsearch/v1', { params: { key: gKey, cx, q, searchType: 'image', num: Math.min(parseInt(count), 10) }, timeout: 10000 });\n return ok(res, { query: q, images: (r.data.items || []).map(i => ({ url: i.link, thumbnail: i.image?.thumbnailLink || '', title: i.title })) });\n }\n const vqdR = await axios.get('https://duckduckgo.com/', { params: { q }, headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 8000 });\n const vqd = vqdR.data.match(/vqd=['\"](\\d+-[^'\"]+)['\"]/)?.[1] || '';\n const imgR = await axios.get('https://duckduckgo.com/i.js', { params: { l: 'us-en', o: 'json', q, vqd, p: '-1' }, headers: { 'User-Agent': 'Mozilla/5.0', Referer: 'https://duckduckgo.com/' }, timeout: 10000 });\n return ok(res, { query: q, images: (imgR.data.results || []).slice(0, parseInt(count)).map(r => ({ url: r.image, thumbnail: r.thumbnail, title: r.title })) });\n }\n\n if (path === '/image/generate') {\n const prompt = req.body?.prompt || req.query?.prompt;\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const style = req.body?.style || req.query?.style || 'realistic';\n const styleMap = { anime: 'anime style,', art: 'digital art,', cartoon: 'cartoon,', sketch: 'pencil sketch,' };\n const fullPrompt = ((styleMap[style] || '') + ' ' + prompt).trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/images/generations', { model: 'dall-e-3', prompt: fullPrompt, n: 1, size: '1024x1024' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 60000 });\n return ok(res, { prompt: fullPrompt, image_url: r.data.data[0]?.url, model: 'dall-e-3' });\n }\n const seed = Math.floor(Math.random() * 1000000);\n const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(fullPrompt)}?width=1024&height=1024&model=flux&seed=${seed}&nologo=true`;\n return ok(res, { prompt: fullPrompt, image_url: imageUrl, model: 'flux', seed });\n }\n\n // AI\n if (path === '/ai/chat') {\n const { message, system = 'You are a helpful AI assistant.' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 25000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'gpt-3.5-turbo', tokens_used: r.data.usage?.total_tokens || 0 });\n }\n const r = await axios.post('https://api.deepinfra.com/v1/openai/chat/completions', { model: 'meta-llama/Meta-Llama-3-8B-Instruct', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Content-Type': 'application/json' }, timeout: 25000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'llama-3-8b' });\n }\n\n if (path === '/ai/generate') {\n const { prompt, type = 'default', length = 'medium', tone = 'professional' } = req.body || {};\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const typeMap = { article: 'Write a detailed article about:', code: 'Write clean code for:', story: 'Write a short story about:', email: 'Write a professional email for:' };\n const maxTokens = { short: 300, medium: 700, long: 1400 }[length] || 700;\n const fullPrompt = `${typeMap[type] || ''} ${prompt} (Tone: ${tone})`.trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: fullPrompt }], max_tokens: maxTokens }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 });\n const output = r.data.choices[0]?.message?.content || '';\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length });\n }\n const r = await axios.get(`https://text.pollinations.ai/${encodeURIComponent(fullPrompt)}`, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 25000 });\n const output = typeof r.data === 'string' ? r.data : JSON.stringify(r.data);\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length, model: 'pollinations' });\n }\n\n if (path === '/ai/tts') {\n const text = req.body?.text || req.query?.text;\n const voice = req.body?.voice || req.query?.voice || 'Brian';\n if (!text) return err(res, 'Missing: text', 400);\n if (text.length > 3000) return err(res, 'Text too long. Max 3000 chars.', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/audio/speech', { model: 'tts-1', input: text, voice: 'alloy' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, responseType: 'arraybuffer', timeout: 25000 });\n res.setHeader('Content-Type', 'audio/mpeg');\n return res.send(Buffer.from(r.data));\n }\n const voices = ['Brian','Amy','Emma','Matthew','Joanna','Joey','Kendra','Kimberly','Salli'];\n const sel = voices.includes(voice) ? voice : 'Brian';\n return ok(res, { text, voice: sel, audio_url: `https://api.streamelements.com/kappa/v2/speech?voice=${sel}&text=${encodeURIComponent(text)}`, format: 'mp3' });\n }\n\n if (path === '/ai/translate') {\n const { text, to, from = 'auto' } = req.query;\n if (!text) return err(res, 'Missing: text', 400);\n if (!to) return err(res, 'Missing: to', 400);\n try {\n const r = await axios.get('https://translate.googleapis.com/translate_a/single', { params: { client: 'gtx', sl: from === 'auto' ? 'auto' : from, tl: to, dt: 't', q: text }, headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data[0]?.map(s => s[0]).join('') || '', from: r.data[2] || from, to });\n } catch (_) {\n const r = await axios.get('https://api.mymemory.translated.net/get', { params: { q: text, langpair: `${from}|${to}` }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data.responseData.translatedText, from, to });\n }\n }\n\n // UTILITY\n if (path === '/weather') {\n const { city, units = 'metric' } = req.query;\n if (!city) return err(res, 'Missing: city', 400);\n const geo = await axios.get('https://nominatim.openstreetmap.org/search', { params: { q: city, format: 'json', limit: 1 }, headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 8000 });\n if (!geo.data?.length) return err(res, `City not found: ${city}`, 404);\n const { lat, lon, display_name } = geo.data[0];\n const w = await axios.get('https://api.open-meteo.com/v1/forecast', { params: { latitude: lat, longitude: lon, current: 'temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,precipitation', daily: 'weather_code,temperature_2m_max,temperature_2m_min', temperature_unit: units === 'imperial' ? 'fahrenheit' : 'celsius', wind_speed_unit: units === 'imperial' ? 'mph' : 'kmh', timezone: 'auto', forecast_days: 7 }, timeout: 10000 });\n const c = w.data.current, unit = units === 'imperial' ? '°F' : '°C';\n const WMO = { 0: 'Clear sky', 1: 'Mainly clear', 2: 'Partly cloudy', 3: 'Overcast', 45: 'Foggy', 61: 'Slight rain', 63: 'Moderate rain', 65: 'Heavy rain', 80: 'Showers', 95: 'Thunderstorm' };\n return ok(res, { city: display_name.split(',')[0], temperature: `${c.temperature_2m}${unit}`, feels_like: `${c.apparent_temperature}${unit}`, condition: WMO[c.weather_code] || 'Unknown', humidity: `${c.relative_humidity_2m}%`, wind_speed: `${c.wind_speed_10m} ${units === 'imperial' ? 'mph' : 'km/h'}`, forecast: w.data.daily.time.map((d, i) => ({ date: d, high: `${w.data.daily.temperature_2m_max[i]}${unit}`, low: `${w.data.daily.temperature_2m_min[i]}${unit}` })) });\n }\n\n if (path === '/currency/convert') {\n const { from = 'USD', to, amount = '1', action } = req.query;\n const r = await axios.get(`https://open.er-api.com/v6/latest/${from.toUpperCase()}`, { timeout: 10000 });\n if (action === 'rates') return ok(res, { base: from.toUpperCase(), rates: r.data.rates, last_updated: r.data.time_last_update_utc });\n if (!to) return err(res, 'Missing: to', 400);\n const rate = r.data.rates[to.toUpperCase()];\n if (!rate) return err(res, `Currency not found: ${to}`, 400);\n const amt = parseFloat(amount);\n return ok(res, { from: from.toUpperCase(), to: to.toUpperCase(), amount: amt, rate, result: parseFloat((amt * rate).toFixed(6)), formatted: `${(amt * rate).toFixed(2)} ${to.toUpperCase()}` });\n }\n\n if (path === '/qrcode/generate') {\n const { data, size = '300', color = '000000', bg = 'ffffff', format = 'png' } = req.query;\n if (!data) return err(res, 'Missing: data', 400);\n const sizeNum = Math.min(Math.max(parseInt(size) || 300, 100), 1000);\n const opts = { width: sizeNum, margin: 2, color: { dark: `#${color}`, light: `#${bg}` } };\n if (format === 'base64') { const dataUrl = await QRCode.toDataURL(data, opts); return ok(res, { data, image: dataUrl }); }\n if (format === 'svg') { res.setHeader('Content-Type', 'image/svg+xml'); return res.send(await QRCode.toString(data, { ...opts, type: 'svg' })); }\n res.setHeader('Content-Type', 'image/png');\n return res.send(await QRCode.toBuffer(data, opts));\n }\n\n if (path === '/ip/lookup') {\n let { ip } = req.query;\n if (!ip) ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || '';\n const r = await axios.get(`https://ipwho.is/${ip}`, { timeout: 8000 });\n if (!r.data.success) return err(res, 'IP lookup failed.', 400);\n return ok(res, { ip: r.data.ip, country: r.data.country, country_code: r.data.country_code, region: r.data.region, city: r.data.city, latitude: r.data.latitude, longitude: r.data.longitude, timezone: r.data.timezone?.id || '', isp: r.data.connection?.isp || '' });\n }\n\n if (path === '/url/shorten') {\n const url = req.body?.url || req.query?.url;\n const alias = req.body?.alias || req.query?.alias || Math.random().toString(36).slice(2, 8);\n if (!url) return err(res, 'Missing: url', 400);\n try { new URL(url); } catch { return err(res, 'Invalid URL format', 400); }\n const db = await getDB();\n const ref = db.collection('short_urls').doc(alias);\n const snap = await ref.get();\n if (!snap.exists) await ref.set({ url, alias, clicks: 0, created_at: new Date() });\n return ok(res, { original: url, short: `https://api-blacknode-zw.vercel.app/api/s/${alias}`, alias, clicks: snap.exists ? snap.data().clicks : 0 });\n }\n\n if (path === '/news') {\n const { q, category = 'general', limit = '10' } = req.query;\n const ndKey = process.env.NEWSDATA_API_KEY;\n if (ndKey) {\n const params = { apikey: ndKey, language: 'en', size: Math.min(parseInt(limit), 50) };\n if (q) params.q = q; else params.category = category;\n const r = await axios.get('https://newsdata.io/api/1/news', { params, timeout: 12000 });\n if (r.data.status === 'success') return ok(res, { query: q || category, articles: (r.data.results || []).map(a => ({ title: a.title || '', description: a.description || '', url: a.link || '', image: a.image_url || '', source: a.source_id || '', published_at: a.pubDate || '' })) });\n }\n const RSS = { general: 'https://feeds.bbci.co.uk/news/rss.xml', technology: 'https://feeds.bbci.co.uk/news/technology/rss.xml', sports: 'https://feeds.bbci.co.uk/sport/rss.xml', business: 'https://feeds.bbci.co.uk/news/business/rss.xml' };\n const r = await axios.get(RSS[category] || RSS.general, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 12000 });\n const items = [];\n for (const m of r.data.matchAll(/<item>([\\s\\S]*?)<\\/item>/g)) {\n const i = m[1];\n const title = (i.match(/<title><!\\[CDATA\\[(.+?)\\]\\]>/) || i.match(/<title>(.+?)<\\/title>/))?.[1] || '';\n const link = i.match(/<link>(.+?)<\\/link>/)?.[1] || '';\n if (title) items.push({ title: title.trim(), url: link.trim(), source: 'BBC News' });\n if (items.length >= parseInt(limit)) break;\n }\n return ok(res, { query: q || category, articles: items });\n }\n\n if (path === '/dictionary') {\n const { word, lang = 'en' } = req.query;\n if (!word) return err(res, 'Missing: word', 400);\n const r = await axios.get(`https://api.dictionaryapi.dev/api/v2/entries/${lang}/${encodeURIComponent(word.trim().toLowerCase())}`, { timeout: 10000 });\n const entry = r.data[0];\n return ok(res, { word: entry.word, phonetic: entry.phonetic || '', meanings: (entry.meanings || []).map(m => ({ part_of_speech: m.partOfSpeech, definitions: (m.definitions || []).slice(0, 3).map(d => ({ definition: d.definition, example: d.example || '' })), synonyms: (m.synonyms || []).slice(0, 8) })) });\n }\n\n if (path === '/github/user') {\n const { username, action = 'user', repo } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const headers = process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 'User-Agent': 'BlackNodeZW/1.0' } : { 'User-Agent': 'BlackNodeZW/1.0' };\n if (action === 'repos') {\n const r = await axios.get(`https://api.github.com/users/${username}/repos?sort=stars&per_page=20`, { headers, timeout: 10000 });\n return ok(res, { username, repos: r.data.map(r => ({ name: r.name, description: r.description || '', url: r.html_url, language: r.language || '', stars: r.stargazers_count, forks: r.forks_count })) });\n }\n const r = await axios.get(`https://api.github.com/users/${username}`, { headers, timeout: 10000 });\n const u = r.data;\n return ok(res, { username: u.login, name: u.name || '', bio: u.bio || '', avatar: u.avatar_url, url: u.html_url, followers: u.followers, following: u.following, public_repos: u.public_repos, location: u.location || '' });\n }\n\n if (path === '/fun') {\n const { type = 'quote', category = 'general' } = req.query;\n if (type === 'quote') { const r = await axios.get('https://zenquotes.io/api/random', { timeout: 8000 }); return ok(res, { type: 'quote', content: r.data[0].q, author: r.data[0].a }); }\n if (type === 'joke') { const r = await axios.get(`https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,racist,sexist`, { timeout: 8000 }); return ok(res, { type: 'joke', category: r.data.category, joke: r.data.joke || null, setup: r.data.setup || null, delivery: r.data.delivery || null }); }\n if (type === 'fact') { const r = await axios.get('https://uselessfacts.jsph.pl/api/v2/facts/random?language=en', { timeout: 8000 }); return ok(res, { type: 'fact', content: r.data.text }); }\n if (type === 'trivia') { const r = await axios.get('https://opentdb.com/api.php?amount=1&type=multiple', { timeout: 8000 }); const q = r.data.results?.[0]; if (q) return ok(res, { type: 'trivia', category: q.category, difficulty: q.difficulty, question: q.question, correct_answer: q.correct_answer, incorrect_answers: q.incorrect_answers }); }\n return err(res, `Invalid type: ${type}. Use: quote, joke, fact, trivia`, 400);\n }\n\n if (path === '/spotify/track') {\n const { url, action, q } = req.query;\n const clientId = process.env.SPOTIFY_CLIENT_ID, clientSecret = process.env.SPOTIFY_CLIENT_SECRET;\n if (!clientId || !clientSecret) return err(res, 'Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in environment variables.');\n const tokenR = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({ grant_type: 'client_credentials' }), { headers: { 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 });\n const headers = { 'Authorization': `Bearer ${tokenR.data.access_token}` };\n if (action === 'search' && q) {\n const r = await axios.get('https://api.spotify.com/v1/search', { params: { q, type: 'track', limit: 10, market: 'US' }, headers, timeout: 10000 });\n return ok(res, { query: q, tracks: (r.data.tracks?.items || []).map(t => ({ id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', preview_url: t.preview_url || '', spotify_url: t.external_urls?.spotify || '' })) });\n }\n if (!url) return err(res, 'Missing: url or use action=search&q=query', 400);\n const trackId = url.match(/track\\/([a-zA-Z0-9]+)/)?.[1];\n if (!trackId) return err(res, 'Invalid Spotify URL', 400);\n const r = await axios.get(`https://api.spotify.com/v1/tracks/${trackId}`, { headers, timeout: 10000 });\n const t = r.data;\n return ok(res, { id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', duration_ms: t.duration_ms, preview_url: t.preview_url || '', popularity: t.popularity, spotify_url: t.external_urls?.spotify || '' });\n }\n\n if (path === '/screenshot') {\n const { url, width = '1280', height = '720' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const r = await axios.get(`https://image.thum.io/get/width/${width}/crop/${height}/noanimate/${encodeURIComponent(url)}`, { responseType: 'arraybuffer', timeout: 20000, headers: { 'User-Agent': 'BlackNodeZW/1.0' } });\n res.setHeader('Content-Type', 'image/jpeg');\n return res.send(Buffer.from(r.data));\n }\n\n return res.status(404).json({ status: 404, error: `Endpoint not found: ${path}`, docs: 'https://api-blacknode-zw.vercel.app' });\n\n } catch (e) {\n return err(res, e.message);\n }\n};\n"}, "0d30e28736b1f7d161bd131d6c29d54a238a780e": {"t": 1785767106.2535834, "n": 33003, "h": "0d208f2d9f9e28c1", "c": "// api/index.js - Single unified handler for ALL endpoints\n// Keeps everything within Vercel free plan 12-function limit\n\nconst axios = require('axios');\nconst QRCode = require('qrcode');\n\nfunction cors(res) {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type,x-api-key,Authorization');\n}\nfunction ok(res, data) { return res.status(200).json({ status: 200, data }); }\nfunction err(res, msg, code = 500) { return res.status(code).json({ status: code, error: msg }); }\n\nlet _db = null;\nasync function getDB() {\n if (_db) return _db;\n const { initializeApp, getApps, cert } = require('firebase-admin/app');\n const { getFirestore } = require('firebase-admin/firestore');\n if (!getApps().length) {\n if (process.env.FIREBASE_SERVICE_ACCOUNT) {\n initializeApp({ credential: cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT)) });\n } else { initializeApp({ projectId: 'apiblacknodezw' }); }\n }\n _db = getFirestore();\n return _db;\n}\n\nasync function authCheck(req, res) {\n if (req.method === 'OPTIONS') { res.status(200).end(); return false; }\n const key = req.headers['x-api-key'];\n if (!key) { err(res, 'Missing API key in x-api-key header.', 401); return false; }\n try {\n const db = await getDB();\n const snap = await db.collection('users').where('apiKey', '==', key).limit(1).get();\n if (snap.empty) { err(res, 'Invalid API key.', 403); return false; }\n const user = snap.docs[0].data();\n const limit = user.plan === 'pro' ? 50000 : 1000;\n if ((user.requests || 0) >= limit) { err(res, 'Monthly limit reached. Upgrade to Pro.', 429); return false; }\n snap.docs[0].ref.update({ requests: (user.requests || 0) + 1 }).catch(() => {});\n return true;\n } catch (e) { return true; }\n}\n\nmodule.exports = async (req, res) => {\n cors(res);\n if (req.method === 'OPTIONS') return res.status(200).end();\n const path = req.url.replace(/\\?.*/, '').replace(/^\\/api/, '');\n\n // No auth routes\n if (path === '' || path === '/' || path === '/status') {\n return res.json({ status: 'online', name: 'Black Node ZW API', version: '1.0.0', base_url: 'https://api.blacknodezw.vercel.app', endpoints: 30, timestamp: new Date().toISOString() });\n }\n const shortMatch = path.match(/^\\/s\\/(.+)$/);\n if (shortMatch) {\n try {\n const db = await getDB();\n const snap = await db.collection('short_urls').doc(shortMatch[1]).get();\n if (!snap.exists) return res.status(404).send('Link not found');\n snap.ref.update({ clicks: (snap.data().clicks || 0) + 1 }).catch(() => {});\n return res.redirect(301, snap.data().url);\n } catch (e) { return res.redirect('https://api.blacknodezw.vercel.app'); }\n }\n\n if (!(await authCheck(req, res))) return;\n\n try {\n // TIKTOK\n if (path === '/tiktok/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const r = await axios.post('https://www.tikwm.com/api/', new URLSearchParams({ url, hd: '1' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n const d = r.data?.data;\n if (r.data?.code === 0 && d) return ok(res, { title: d.title || '', author: '@' + (d.author?.unique_id || ''), thumbnail: d.cover || '', download_url_hd: d.hdplay || d.play || '', download_url_sd: d.play || '', music_url: d.music || '', duration: d.duration || 0, play_count: d.play_count || 0, like_count: d.digg_count || 0 });\n return err(res, 'Could not fetch video. Check the URL.');\n }\n\n if (path === '/tiktok/user') {\n const { username, include_videos = 'false', video_count = '10' } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n const r = await axios.post('https://www.tikwm.com/api/user/info', new URLSearchParams({ unique_id: clean }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (r.data?.code !== 0) return err(res, 'User not found or account is private.');\n const u = r.data.data?.user || {}, s = r.data.data?.stats || {};\n const result = { username: u.uniqueId || clean, nickname: u.nickname || '', bio: u.signature || '', avatar: u.avatarLarger || '', verified: u.verified || false, private: u.privateAccount || false, followers: s.followerCount || 0, following: s.followingCount || 0, likes: s.heartCount || 0, video_count: s.videoCount || 0, profile_url: `https://www.tiktok.com/@${clean}` };\n if (include_videos === 'true') {\n try {\n const vr = await axios.post('https://www.tikwm.com/api/user/posts', new URLSearchParams({ unique_id: clean, count: Math.min(parseInt(video_count), 30), cursor: '0' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (vr.data?.code === 0) result.recent_videos = (vr.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0, url: `https://www.tiktok.com/@${clean}/video/${v.video_id}` }));\n } catch (_) {}\n }\n return ok(res, result);\n }\n\n if (path === '/tiktok/search') {\n const { q, count = '20', cursor = '0' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const r = await axios.post('https://www.tikwm.com/api/feed/search', new URLSearchParams({ keywords: q, count: Math.min(parseInt(count), 50), cursor }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (r.data?.code !== 0) return err(res, 'Search failed.');\n return ok(res, { query: q, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, like_count: v.digg_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' }, url: `https://www.tiktok.com/@${v.author?.unique_id}/video/${v.video_id}` })) });\n }\n\n if (path === '/tiktok/trending') {\n const { region = 'US', count = '20' } = req.query;\n const r = await axios.post('https://www.tikwm.com/api/feed/list', new URLSearchParams({ region, count: Math.min(parseInt(count), 50) }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 });\n if (r.data?.code !== 0) return err(res, 'Could not fetch trending.');\n return ok(res, { region, total: r.data.data?.videos?.length || 0, videos: (r.data.data?.videos || []).map(v => ({ id: v.video_id, title: v.title || '', cover: v.cover || '', download_url: v.hdplay || v.play || '', duration: v.duration || 0, play_count: v.play_count || 0, author: { username: v.author?.unique_id || '', nickname: v.author?.nickname || '' } })) });\n }\n\n // YOUTUBE\n if (path === '/youtube/mp3' || path === '/youtube/mp4') {\n const { url, quality = '720' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = url.match(/(?:v=|youtu\\.be\\/|embed\\/)([^&\\n?#]+)/)?.[1];\n if (!videoId) return err(res, 'Invalid YouTube URL', 400);\n const isAudio = path === '/youtube/mp3';\n const [cobalt, oembed] = await Promise.allSettled([\n axios.post('https://api.cobalt.tools/api/json', isAudio ? { url: `https://www.youtube.com/watch?v=${videoId}`, isAudioOnly: true, aFormat: 'mp3', audioBitrate: '320' } : { url: `https://www.youtube.com/watch?v=${videoId}`, vQuality: quality }, { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, timeout: 20000 }),\n axios.get(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`, { timeout: 6000 })\n ]);\n const title = oembed.value?.data?.title || 'YouTube';\n const channel = oembed.value?.data?.author_name || 'YouTube';\n const thumbnail = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;\n const cd = cobalt.value?.data;\n if (cd?.status === 'stream' || cd?.status === 'redirect') return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.url, quality: isAudio ? '320kbps' : quality + 'p', format: isAudio ? 'mp3' : 'mp4' });\n if (cd?.status === 'picker' && cd.picker?.length) return ok(res, { video_id: videoId, title, channel, thumbnail, download_url: cd.picker[0].url, quality: quality + 'p', format: 'mp4' });\n return err(res, 'Could not fetch. Try again.');\n }\n\n if (path === '/youtube/info') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const videoId = url.match(/(?:v=|youtu\\.be\\/|embed\\/)([^&\\n?#]+)/)?.[1];\n if (!videoId) return err(res, 'Invalid YouTube URL', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (ytKey) {\n const r = await axios.get('https://www.googleapis.com/youtube/v3/videos', { params: { key: ytKey, id: videoId, part: 'snippet,statistics' }, timeout: 10000 });\n const item = r.data.items?.[0];\n if (item) return ok(res, { video_id: videoId, title: item.snippet.title, channel: item.snippet.channelTitle, thumbnail: item.snippet.thumbnails?.high?.url || '', views: parseInt(item.statistics.viewCount) || 0, likes: parseInt(item.statistics.likeCount) || 0, comments: parseInt(item.statistics.commentCount) || 0, published_at: item.snippet.publishedAt });\n }\n const r = await axios.get(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`, { timeout: 8000 });\n return ok(res, { video_id: videoId, title: r.data.title, channel: r.data.author_name, thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg` });\n }\n\n if (path === '/youtube/channel') {\n const { channel } = req.query;\n if (!channel) return err(res, 'Missing: channel', 400);\n const ytKey = process.env.YOUTUBE_API_KEY;\n if (!ytKey) return err(res, 'Set YOUTUBE_API_KEY in environment variables.');\n const search = await axios.get('https://www.googleapis.com/youtube/v3/search', { params: { key: ytKey, q: channel.replace(/^@/, ''), type: 'channel', part: 'id', maxResults: 1 }, timeout: 10000 });\n const channelId = search.data.items?.[0]?.id?.channelId;\n if (!channelId) return err(res, 'Channel not found.', 404);\n const stats = await axios.get('https://www.googleapis.com/youtube/v3/channels', { params: { key: ytKey, id: channelId, part: 'snippet,statistics' }, timeout: 10000 });\n const ch = stats.data.items?.[0];\n if (!ch) return err(res, 'Channel not found.', 404);\n return ok(res, { channel_id: channelId, title: ch.snippet.title, description: ch.snippet.description, avatar: ch.snippet.thumbnails?.high?.url || '', subscribers: parseInt(ch.statistics.subscriberCount) || 0, total_views: parseInt(ch.statistics.viewCount) || 0, video_count: parseInt(ch.statistics.videoCount) || 0 });\n }\n\n // INSTAGRAM\n if (path === '/instagram/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const r = await axios.post('https://api.cobalt.tools/api/json', { url }, { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, timeout: 20000 });\n if (r.data.status === 'stream' || r.data.status === 'redirect') return ok(res, { type: 'video', download_url: r.data.url });\n if (r.data.status === 'picker') return ok(res, { type: 'gallery', items: r.data.picker.map(p => ({ type: p.type, url: p.url })) });\n return err(res, 'Could not download. Make sure the post is public.');\n }\n\n if (path === '/instagram/user') {\n const { username } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const clean = username.replace(/^@/, '');\n const r = await axios.get(`https://www.instagram.com/api/v1/users/web_profile_info/?username=${clean}`, { headers: { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)', 'X-IG-App-ID': '936619743392459', Referer: `https://www.instagram.com/${clean}/` }, timeout: 15000 });\n const u = r.data?.data?.user;\n if (!u) return err(res, 'User not found or account is private.', 404);\n return ok(res, { username: u.username, full_name: u.full_name || '', bio: u.biography || '', avatar: u.profile_pic_url_hd || u.profile_pic_url || '', verified: u.is_verified || false, private: u.is_private || false, followers: u.edge_followed_by?.count || 0, following: u.edge_follow?.count || 0, posts: u.edge_owner_to_timeline_media?.count || 0 });\n }\n\n // OTHER DOWNLOADERS\n if (path === '/facebook/download' || path === '/twitter/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const r = await axios.post('https://api.cobalt.tools/api/json', { url }, { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, timeout: 20000 });\n if (r.data.status === 'stream' || r.data.status === 'redirect') return ok(res, { download_url: r.data.url });\n if (r.data.status === 'picker') return ok(res, { items: r.data.picker.map(p => ({ type: p.type, url: p.url })) });\n return err(res, 'Could not download. Make sure the content is public.');\n }\n\n if (path === '/pinterest/download') {\n const { url } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const cheerio = require('cheerio');\n const r = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0', Accept: 'text/html' }, maxRedirects: 5, timeout: 15000 });\n const $ = cheerio.load(r.data);\n const videoUrl = $('meta[property=\"og:video\"]').attr('content') || '';\n const imageUrl = $('meta[property=\"og:image\"]').attr('content') || '';\n if (!videoUrl && !imageUrl) return err(res, 'No media found. Make sure the pin is public.');\n const hdImage = imageUrl.replace(/\\/\\d+x\\//, '/originals/');\n return ok(res, { type: videoUrl ? 'video' : 'image', title: $('meta[property=\"og:title\"]').attr('content') || '', download_url: videoUrl || hdImage, thumbnail: imageUrl, hd_image: hdImage });\n }\n\n // IMAGE\n if (path === '/image/search') {\n const { q, count = '10' } = req.query;\n if (!q) return err(res, 'Missing: q', 400);\n const gKey = process.env.GOOGLE_SEARCH_API_KEY, cx = process.env.GOOGLE_SEARCH_CX;\n if (gKey && cx) {\n const r = await axios.get('https://www.googleapis.com/customsearch/v1', { params: { key: gKey, cx, q, searchType: 'image', num: Math.min(parseInt(count), 10) }, timeout: 10000 });\n return ok(res, { query: q, images: (r.data.items || []).map(i => ({ url: i.link, thumbnail: i.image?.thumbnailLink || '', title: i.title })) });\n }\n const vqdR = await axios.get('https://duckduckgo.com/', { params: { q }, headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 8000 });\n const vqd = vqdR.data.match(/vqd=['\"](\\d+-[^'\"]+)['\"]/)?.[1] || '';\n const imgR = await axios.get('https://duckduckgo.com/i.js', { params: { l: 'us-en', o: 'json', q, vqd, p: '-1' }, headers: { 'User-Agent': 'Mozilla/5.0', Referer: 'https://duckduckgo.com/' }, timeout: 10000 });\n return ok(res, { query: q, images: (imgR.data.results || []).slice(0, parseInt(count)).map(r => ({ url: r.image, thumbnail: r.thumbnail, title: r.title })) });\n }\n\n if (path === '/image/generate') {\n const prompt = req.body?.prompt || req.query?.prompt;\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const style = req.body?.style || req.query?.style || 'realistic';\n const styleMap = { anime: 'anime style,', art: 'digital art,', cartoon: 'cartoon,', sketch: 'pencil sketch,' };\n const fullPrompt = ((styleMap[style] || '') + ' ' + prompt).trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/images/generations', { model: 'dall-e-3', prompt: fullPrompt, n: 1, size: '1024x1024' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 60000 });\n return ok(res, { prompt: fullPrompt, image_url: r.data.data[0]?.url, model: 'dall-e-3' });\n }\n const seed = Math.floor(Math.random() * 1000000);\n const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(fullPrompt)}?width=1024&height=1024&model=flux&seed=${seed}&nologo=true`;\n return ok(res, { prompt: fullPrompt, image_url: imageUrl, model: 'flux', seed });\n }\n\n // AI\n if (path === '/ai/chat') {\n const { message, system = 'You are a helpful AI assistant.' } = req.body || {};\n if (!message) return err(res, 'Missing: message', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 25000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'gpt-3.5-turbo', tokens_used: r.data.usage?.total_tokens || 0 });\n }\n const r = await axios.post('https://api.deepinfra.com/v1/openai/chat/completions', { model: 'meta-llama/Meta-Llama-3-8B-Instruct', messages: [{ role: 'system', content: system }, { role: 'user', content: message }], max_tokens: 1024 }, { headers: { 'Content-Type': 'application/json' }, timeout: 25000 });\n return ok(res, { message, response: r.data.choices[0]?.message?.content || '', model: 'llama-3-8b' });\n }\n\n if (path === '/ai/generate') {\n const { prompt, type = 'default', length = 'medium', tone = 'professional' } = req.body || {};\n if (!prompt) return err(res, 'Missing: prompt', 400);\n const typeMap = { article: 'Write a detailed article about:', code: 'Write clean code for:', story: 'Write a short story about:', email: 'Write a professional email for:' };\n const maxTokens = { short: 300, medium: 700, long: 1400 }[length] || 700;\n const fullPrompt = `${typeMap[type] || ''} ${prompt} (Tone: ${tone})`.trim();\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: fullPrompt }], max_tokens: maxTokens }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 });\n const output = r.data.choices[0]?.message?.content || '';\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length });\n }\n const r = await axios.get(`https://text.pollinations.ai/${encodeURIComponent(fullPrompt)}`, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 25000 });\n const output = typeof r.data === 'string' ? r.data : JSON.stringify(r.data);\n return ok(res, { prompt, type, output, word_count: output.split(/\\s+/).length, model: 'pollinations' });\n }\n\n if (path === '/ai/tts') {\n const text = req.body?.text || req.query?.text;\n const voice = req.body?.voice || req.query?.voice || 'Brian';\n if (!text) return err(res, 'Missing: text', 400);\n if (text.length > 3000) return err(res, 'Text too long. Max 3000 chars.', 400);\n const openaiKey = process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const r = await axios.post('https://api.openai.com/v1/audio/speech', { model: 'tts-1', input: text, voice: 'alloy' }, { headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, responseType: 'arraybuffer', timeout: 25000 });\n res.setHeader('Content-Type', 'audio/mpeg');\n return res.send(Buffer.from(r.data));\n }\n const voices = ['Brian','Amy','Emma','Matthew','Joanna','Joey','Kendra','Kimberly','Salli'];\n const sel = voices.includes(voice) ? voice : 'Brian';\n return ok(res, { text, voice: sel, audio_url: `https://api.streamelements.com/kappa/v2/speech?voice=${sel}&text=${encodeURIComponent(text)}`, format: 'mp3' });\n }\n\n if (path === '/ai/translate') {\n const { text, to, from = 'auto' } = req.query;\n if (!text) return err(res, 'Missing: text', 400);\n if (!to) return err(res, 'Missing: to', 400);\n try {\n const r = await axios.get('https://translate.googleapis.com/translate_a/single', { params: { client: 'gtx', sl: from === 'auto' ? 'auto' : from, tl: to, dt: 't', q: text }, headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data[0]?.map(s => s[0]).join('') || '', from: r.data[2] || from, to });\n } catch (_) {\n const r = await axios.get('https://api.mymemory.translated.net/get', { params: { q: text, langpair: `${from}|${to}` }, timeout: 10000 });\n return ok(res, { original: text, translated: r.data.responseData.translatedText, from, to });\n }\n }\n\n // UTILITY\n if (path === '/weather') {\n const { city, units = 'metric' } = req.query;\n if (!city) return err(res, 'Missing: city', 400);\n const geo = await axios.get('https://nominatim.openstreetmap.org/search', { params: { q: city, format: 'json', limit: 1 }, headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 8000 });\n if (!geo.data?.length) return err(res, `City not found: ${city}`, 404);\n const { lat, lon, display_name } = geo.data[0];\n const w = await axios.get('https://api.open-meteo.com/v1/forecast', { params: { latitude: lat, longitude: lon, current: 'temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,precipitation', daily: 'weather_code,temperature_2m_max,temperature_2m_min', temperature_unit: units === 'imperial' ? 'fahrenheit' : 'celsius', wind_speed_unit: units === 'imperial' ? 'mph' : 'kmh', timezone: 'auto', forecast_days: 7 }, timeout: 10000 });\n const c = w.data.current, unit = units === 'imperial' ? '°F' : '°C';\n const WMO = { 0: 'Clear sky', 1: 'Mainly clear', 2: 'Partly cloudy', 3: 'Overcast', 45: 'Foggy', 61: 'Slight rain', 63: 'Moderate rain', 65: 'Heavy rain', 80: 'Showers', 95: 'Thunderstorm' };\n return ok(res, { city: display_name.split(',')[0], temperature: `${c.temperature_2m}${unit}`, feels_like: `${c.apparent_temperature}${unit}`, condition: WMO[c.weather_code] || 'Unknown', humidity: `${c.relative_humidity_2m}%`, wind_speed: `${c.wind_speed_10m} ${units === 'imperial' ? 'mph' : 'km/h'}`, forecast: w.data.daily.time.map((d, i) => ({ date: d, high: `${w.data.daily.temperature_2m_max[i]}${unit}`, low: `${w.data.daily.temperature_2m_min[i]}${unit}` })) });\n }\n\n if (path === '/currency/convert') {\n const { from = 'USD', to, amount = '1', action } = req.query;\n const r = await axios.get(`https://open.er-api.com/v6/latest/${from.toUpperCase()}`, { timeout: 10000 });\n if (action === 'rates') return ok(res, { base: from.toUpperCase(), rates: r.data.rates, last_updated: r.data.time_last_update_utc });\n if (!to) return err(res, 'Missing: to', 400);\n const rate = r.data.rates[to.toUpperCase()];\n if (!rate) return err(res, `Currency not found: ${to}`, 400);\n const amt = parseFloat(amount);\n return ok(res, { from: from.toUpperCase(), to: to.toUpperCase(), amount: amt, rate, result: parseFloat((amt * rate).toFixed(6)), formatted: `${(amt * rate).toFixed(2)} ${to.toUpperCase()}` });\n }\n\n if (path === '/qrcode/generate') {\n const { data, size = '300', color = '000000', bg = 'ffffff', format = 'png' } = req.query;\n if (!data) return err(res, 'Missing: data', 400);\n const sizeNum = Math.min(Math.max(parseInt(size) || 300, 100), 1000);\n const opts = { width: sizeNum, margin: 2, color: { dark: `#${color}`, light: `#${bg}` } };\n if (format === 'base64') { const dataUrl = await QRCode.toDataURL(data, opts); return ok(res, { data, image: dataUrl }); }\n if (format === 'svg') { res.setHeader('Content-Type', 'image/svg+xml'); return res.send(await QRCode.toString(data, { ...opts, type: 'svg' })); }\n res.setHeader('Content-Type', 'image/png');\n return res.send(await QRCode.toBuffer(data, opts));\n }\n\n if (path === '/ip/lookup') {\n let { ip } = req.query;\n if (!ip) ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || '';\n const r = await axios.get(`https://ipwho.is/${ip}`, { timeout: 8000 });\n if (!r.data.success) return err(res, 'IP lookup failed.', 400);\n return ok(res, { ip: r.data.ip, country: r.data.country, country_code: r.data.country_code, region: r.data.region, city: r.data.city, latitude: r.data.latitude, longitude: r.data.longitude, timezone: r.data.timezone?.id || '', isp: r.data.connection?.isp || '' });\n }\n\n if (path === '/url/shorten') {\n const url = req.body?.url || req.query?.url;\n const alias = req.body?.alias || req.query?.alias || Math.random().toString(36).slice(2, 8);\n if (!url) return err(res, 'Missing: url', 400);\n try { new URL(url); } catch { return err(res, 'Invalid URL format', 400); }\n const db = await getDB();\n const ref = db.collection('short_urls').doc(alias);\n const snap = await ref.get();\n if (!snap.exists) await ref.set({ url, alias, clicks: 0, created_at: new Date() });\n return ok(res, { original: url, short: `https://api.blacknodezw.vercel.app/api/s/${alias}`, alias, clicks: snap.exists ? snap.data().clicks : 0 });\n }\n\n if (path === '/news') {\n const { q, category = 'general', limit = '10' } = req.query;\n const ndKey = process.env.NEWSDATA_API_KEY;\n if (ndKey) {\n const params = { apikey: ndKey, language: 'en', size: Math.min(parseInt(limit), 50) };\n if (q) params.q = q; else params.category = category;\n const r = await axios.get('https://newsdata.io/api/1/news', { params, timeout: 12000 });\n if (r.data.status === 'success') return ok(res, { query: q || category, articles: (r.data.results || []).map(a => ({ title: a.title || '', description: a.description || '', url: a.link || '', image: a.image_url || '', source: a.source_id || '', published_at: a.pubDate || '' })) });\n }\n const RSS = { general: 'https://feeds.bbci.co.uk/news/rss.xml', technology: 'https://feeds.bbci.co.uk/news/technology/rss.xml', sports: 'https://feeds.bbci.co.uk/sport/rss.xml', business: 'https://feeds.bbci.co.uk/news/business/rss.xml' };\n const r = await axios.get(RSS[category] || RSS.general, { headers: { 'User-Agent': 'BlackNodeZW/1.0' }, timeout: 12000 });\n const items = [];\n for (const m of r.data.matchAll(/<item>([\\s\\S]*?)<\\/item>/g)) {\n const i = m[1];\n const title = (i.match(/<title><!\\[CDATA\\[(.+?)\\]\\]>/) || i.match(/<title>(.+?)<\\/title>/))?.[1] || '';\n const link = i.match(/<link>(.+?)<\\/link>/)?.[1] || '';\n if (title) items.push({ title: title.trim(), url: link.trim(), source: 'BBC News' });\n if (items.length >= parseInt(limit)) break;\n }\n return ok(res, { query: q || category, articles: items });\n }\n\n if (path === '/dictionary') {\n const { word, lang = 'en' } = req.query;\n if (!word) return err(res, 'Missing: word', 400);\n const r = await axios.get(`https://api.dictionaryapi.dev/api/v2/entries/${lang}/${encodeURIComponent(word.trim().toLowerCase())}`, { timeout: 10000 });\n const entry = r.data[0];\n return ok(res, { word: entry.word, phonetic: entry.phonetic || '', meanings: (entry.meanings || []).map(m => ({ part_of_speech: m.partOfSpeech, definitions: (m.definitions || []).slice(0, 3).map(d => ({ definition: d.definition, example: d.example || '' })), synonyms: (m.synonyms || []).slice(0, 8) })) });\n }\n\n if (path === '/github/user') {\n const { username, action = 'user', repo } = req.query;\n if (!username) return err(res, 'Missing: username', 400);\n const headers = process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 'User-Agent': 'BlackNodeZW/1.0' } : { 'User-Agent': 'BlackNodeZW/1.0' };\n if (action === 'repos') {\n const r = await axios.get(`https://api.github.com/users/${username}/repos?sort=stars&per_page=20`, { headers, timeout: 10000 });\n return ok(res, { username, repos: r.data.map(r => ({ name: r.name, description: r.description || '', url: r.html_url, language: r.language || '', stars: r.stargazers_count, forks: r.forks_count })) });\n }\n const r = await axios.get(`https://api.github.com/users/${username}`, { headers, timeout: 10000 });\n const u = r.data;\n return ok(res, { username: u.login, name: u.name || '', bio: u.bio || '', avatar: u.avatar_url, url: u.html_url, followers: u.followers, following: u.following, public_repos: u.public_repos, location: u.location || '' });\n }\n\n if (path === '/fun') {\n const { type = 'quote', category = 'general' } = req.query;\n if (type === 'quote') { const r = await axios.get('https://zenquotes.io/api/random', { timeout: 8000 }); return ok(res, { type: 'quote', content: r.data[0].q, author: r.data[0].a }); }\n if (type === 'joke') { const r = await axios.get(`https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,racist,sexist`, { timeout: 8000 }); return ok(res, { type: 'joke', category: r.data.category, joke: r.data.joke || null, setup: r.data.setup || null, delivery: r.data.delivery || null }); }\n if (type === 'fact') { const r = await axios.get('https://uselessfacts.jsph.pl/api/v2/facts/random?language=en', { timeout: 8000 }); return ok(res, { type: 'fact', content: r.data.text }); }\n if (type === 'trivia') { const r = await axios.get('https://opentdb.com/api.php?amount=1&type=multiple', { timeout: 8000 }); const q = r.data.results?.[0]; if (q) return ok(res, { type: 'trivia', category: q.category, difficulty: q.difficulty, question: q.question, correct_answer: q.correct_answer, incorrect_answers: q.incorrect_answers }); }\n return err(res, `Invalid type: ${type}. Use: quote, joke, fact, trivia`, 400);\n }\n\n if (path === '/spotify/track') {\n const { url, action, q } = req.query;\n const clientId = process.env.SPOTIFY_CLIENT_ID, clientSecret = process.env.SPOTIFY_CLIENT_SECRET;\n if (!clientId || !clientSecret) return err(res, 'Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in environment variables.');\n const tokenR = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({ grant_type: 'client_credentials' }), { headers: { 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 });\n const headers = { 'Authorization': `Bearer ${tokenR.data.access_token}` };\n if (action === 'search' && q) {\n const r = await axios.get('https://api.spotify.com/v1/search', { params: { q, type: 'track', limit: 10, market: 'US' }, headers, timeout: 10000 });\n return ok(res, { query: q, tracks: (r.data.tracks?.items || []).map(t => ({ id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', preview_url: t.preview_url || '', spotify_url: t.external_urls?.spotify || '' })) });\n }\n if (!url) return err(res, 'Missing: url or use action=search&q=query', 400);\n const trackId = url.match(/track\\/([a-zA-Z0-9]+)/)?.[1];\n if (!trackId) return err(res, 'Invalid Spotify URL', 400);\n const r = await axios.get(`https://api.spotify.com/v1/tracks/${trackId}`, { headers, timeout: 10000 });\n const t = r.data;\n return ok(res, { id: t.id, title: t.name, artists: t.artists?.map(a => a.name).join(', '), album: t.album?.name, cover: t.album?.images?.[0]?.url || '', duration_ms: t.duration_ms, preview_url: t.preview_url || '', popularity: t.popularity, spotify_url: t.external_urls?.spotify || '' });\n }\n\n if (path === '/screenshot') {\n const { url, width = '1280', height = '720' } = req.query;\n if (!url) return err(res, 'Missing: url', 400);\n const r = await axios.get(`https://image.thum.io/get/width/${width}/crop/${height}/noanimate/${encodeURIComponent(url)}`, { responseType: 'arraybuffer', timeout: 20000, headers: { 'User-Agent': 'BlackNodeZW/1.0' } });\n res.setHeader('Content-Type', 'image/jpeg');\n return res.send(Buffer.from(r.data));\n }\n\n return res.status(404).json({ status: 404, error: `Endpoint not found: ${path}`, docs: 'https://api.blacknodezw.vercel.app' });\n\n } catch (e) {\n return err(res, e.message);\n }\n};\n\n"}, "9d3785c0e4a9c10f6c2a04d2cadfcca08023270c": {"t": 1785767108.2550652, "n": 1, "h": "adc83b19e793491b", "c": "\n"}}}}