LOADING
/**
* 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;
}
// CLI
const 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);
});support video, foto, fotolive
untuk download menggunakan --download karena jika langsung pasti 403
bashnode weibo.js "https://weibo.com/3482733354/ReQRv0Vmc"
bashnode weibo.js "https://weibo.com/3482733354/ReQRv0Vmc" --download