weibo downloader with metadata
24 Agustus 2026362 file
weibo.jsjavascript · 293 baris
/** * Weibo Scraper + Downloader (ESM) * * Usage: * node weibo.js "https://weibo.com/xxx/xxx" * node weibo.js "https://weibo.com/xxx/xxx" --download */import { mkdir } from 'node:fs/promises';import { join } from 'node:path';import { createWriteStream } from 'node:fs';import { pipeline } from 'node:stream/promises';import { Readable } from 'node:stream';const UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';const HEADERS = { 'User-Agent': UA, Accept: 'application/json, text/plain, */*', Referer: 'https://m.weibo.cn/', 'X-Requested-With': 'XMLHttpRequest', 'MWeibo-Pwa': '1',};const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';function base62Decode(str) { let num = 0n; for (const c of str) { num = num * 62n + BigInt(BASE62.indexOf(c)); } return num.toString();}function midToId(mid) { if (/^\d+$/.test(mid)) return mid; let result = ''; for (let i = mid.length; i > 0; i -= 4) { const start = Math.max(0, i - 4); let num = base62Decode(mid.slice(start, i)); if (start > 0) num = num.padStart(7, '0'); result = num + result; } return result.replace(/^0+/, '') || '0';}function parseWeiboUrl(url) { url = url.trim(); if (url.includes('t.cn/')) return { type: 'short', shortUrl: url }; let m = url.match(/weibo\.com\/(?:u\/)?(\d+)\/([A-Za-z0-9]+)/); if (m) return { uid: m[1], mid: m[2], id: midToId(m[2]) }; m = url.match(/weibo\.com\/(?:detail|status)\/(\d+)/); if (m) return { id: m[1] }; m = url.match(/m\.weibo\.cn\/(?:detail|status)\/([A-Za-z0-9]+)/); if (m) { const v = m[1]; return /^\d+$/.test(v) ? { id: v } : { mid: v, id: midToId(v) }; } m = url.match(/m\.weibo\.cn\/(\d+)\/([A-Za-z0-9]+)/); if (m) return { uid: m[1], mid: m[2], id: midToId(m[2]) }; if (/^[A-Za-z0-9]{8,12}$/.test(url)) return { mid: url, id: midToId(url) }; if (/^\d{15,20}$/.test(url)) return { id: url }; throw new Error('URL tidak dikenali');}async function resolveShortUrl(shortUrl) { const res = await fetch(shortUrl, { method: 'HEAD', redirect: 'manual', headers: { 'User-Agent': UA }, }); const loc = res.headers.get('location'); if (!loc) throw new Error('Gagal resolve short link'); return loc;}async function fetchStatus(idOrMid, cookie = '') { const id = /^\d+$/.test(idOrMid) ? idOrMid : midToId(idOrMid); const headers = { ...HEADERS }; if (cookie) headers.Cookie = cookie; const res = await fetch(`https://m.weibo.cn/statuses/show?id=${id}`, { headers }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); if (json.ok !== 1) throw new Error(json.msg || 'Gagal ambil data'); return json.data;}function extractMedia(data) { const medias = []; if (data.pics?.length) { for (const pic of data.pics) { const url = pic.large?.url || pic.url; medias.push({ type: 'image', url: url?.replace(/\/orj\d+\//, '/large/') || url, thumbnail: pic.url, livePhoto: pic.videoSrc || null, }); } } if (data.pic_infos) { for (const info of Object.values(data.pic_infos)) { const largest = info.largest || info.original || info.large || info.mw2000; medias.push({ type: 'image', url: largest?.url, thumbnail: info.thumbnail?.url, livePhoto: info.video_src || null, }); } } const pi = data.page_info; if (pi?.type === 'video' || pi?.media_info) { const mi = pi.media_info || {}; const urls = pi.urls || {}; const list = []; if (urls.mp4_720p_mp4) list.push({ quality: '720p', url: urls.mp4_720p_mp4 }); if (urls.mp4_hd_mp4 || mi.stream_url_hd) { list.push({ quality: 'HD', url: urls.mp4_hd_mp4 || mi.stream_url_hd }); } if (urls.mp4_ld_mp4 || mi.stream_url) { list.push({ quality: 'LD', url: urls.mp4_ld_mp4 || mi.stream_url }); } if (mi.playback_list) { for (const item of mi.playback_list) { if (item.play_info?.url) { list.push({ quality: item.meta?.quality_desc || item.meta?.label || 'unknown', url: item.play_info.url, }); } } } const seen = new Set(); const unique = list.filter((q) => { if (seen.has(q.url)) return false; seen.add(q.url); return true; }); if (unique.length) { medias.push({ type: 'video', url: unique[0].url, quality: unique[0].quality, allQualities: unique, duration: mi.duration, cover: pi.page_pic?.url, title: pi.content1 || pi.page_title, playCount: pi.play_count, }); } } return medias;}function parseMetadata(data) { const u = data.user || {}; const profileUrl = u.profile_url ? u.profile_url.startsWith('http') ? u.profile_url : `https://weibo.com${u.profile_url}` : null; return { id: data.id || data.mid, mid: data.bid || data.mblogid, text: (data.text || '').replace(/<[^>]+>/g, '').trim(), createdAt: data.created_at, source: (data.source || '').replace(/<[^>]+>/g, ''), region: data.region_name, reposts: data.reposts_count, comments: data.comments_count, attitudes: data.attitudes_count, user: { id: u.id, screenName: u.screen_name, profileUrl, avatar: u.avatar_hd || u.profile_image_url, verified: u.verified, verifiedReason: u.verified_reason, followers: u.followers_count_str || u.followers_count, description: u.description, }, medias: extractMedia(data), };}async function downloadFile(url, destPath, cookie = '') { const headers = { 'User-Agent': UA, Accept: '*/*', Referer: 'https://weibo.com/', Origin: 'https://weibo.com', }; if (cookie) headers.Cookie = cookie; let res = await fetch(url, { headers, redirect: 'follow' }); if (!res.ok) { headers.Referer = 'https://m.weibo.cn/'; headers.Origin = 'https://m.weibo.cn'; res = await fetch(url, { headers, redirect: 'follow' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); } await mkdir(join(destPath, '..'), { recursive: true }); await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath)); return destPath;}async function scrapeWeibo(url, options = {}) { const { cookie = '', download = false, outDir = './weibo_download' } = options; let parsed = parseWeiboUrl(url); if (parsed.type === 'short') { const resolved = await resolveShortUrl(parsed.shortUrl); parsed = parseWeiboUrl(resolved); } const data = await fetchStatus(parsed.id || parsed.mid, cookie); const meta = parseMetadata(data); if (download && meta.medias.length) { await mkdir(outDir, { recursive: true }); const safeName = (meta.user.screenName || 'user').replace(/[\/\\?%*:|"<>]/g, '_'); const base = `${safeName}_${meta.id}`; for (let i = 0; i < meta.medias.length; i++) { const m = meta.medias[i]; if (!m.url) continue; const candidates = m.allQualities?.length ? m.allQualities.map((q) => q.url) : [m.url]; const ext = m.type === 'video' ? 'mp4' : 'jpg'; const index = String(i + 1).padStart(2, '0'); const filename = `${base}_${index}.${ext}`; const dest = join(outDir, filename); let success = false; for (const cand of candidates) { try { await downloadFile(cand, dest, cookie); m.localPath = dest; m.url = cand; console.log(`✓ ${filename}`); success = true; break; } catch (e) { console.warn(` gagal kualitas, coba berikutnya... (${e.message})`); } } if (!success) console.error(`✗ Gagal download media #${i + 1}`); } } return meta;}// CLIconst url = process.argv[2];if (!url) { console.error('Usage: node weibo.js <url> [--download]'); process.exit(1);}scrapeWeibo(url, { download: process.argv.includes('--download'),}) .then((meta) => console.log(JSON.stringify(meta, null, 2))) .catch((err) => { console.error('Error:', err.message); process.exit(1); });README.mdmarkdown · 20 baris
Komentar
Masuk untuk ikut berkomentar.
Belum ada komentar.