Last active
January 2, 2023 17:38
-
-
Save jamesliu96/156e8bf8722581122e0ce104d4b096d0 to your computer and use it in GitHub Desktop.
Bilibili Live Danmaku Client for Node.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function exitWithErr(...args) { | |
console.error('error:', ...args); | |
process.exit(-1); | |
} | |
let roomId = process.argv[2]; | |
if (!roomId) exitWithErr('please provide roomId'); | |
const DEBUG = false; | |
const { inflateSync } = require('zlib'); | |
const colors = require('colors'); | |
const { default: axios } = require('axios'); | |
const WebSocket = require('ws'); | |
const cheerio = require('cheerio'); | |
/** @returns {Promise<{token:string,host_list:{host:string}[]}>} */ | |
async function getDanmuInfo() { | |
const { data: html = '' } = await axios.get( | |
`https://live.bilibili.com/${roomId}`, | |
{ | |
headers: { | |
host: 'live.bilibili.com', | |
accept: '*/*', | |
}, | |
} | |
); | |
const pad = 'window.__NEPTUNE_IS_MY_WAIFU__='; | |
const $ = cheerio.load(html); | |
roomId = `${ | |
JSON.parse( | |
$('script') | |
.filter(function () { | |
return $(this).html().startsWith(pad); | |
}) | |
.html() | |
.slice(pad.length) | |
).roomInfoRes.data.room_info.room_id | |
}`; | |
const { data = {} } = await axios.get( | |
`https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=${roomId}` | |
); | |
return data['code'] === 0 ? data['data'] : {}; | |
} | |
const logger = { | |
log(t = '', ...args) { | |
if (DEBUG) | |
console.log( | |
colors.cyan(`> ${t} ${args.map((a) => JSON.stringify(a)).join('\t')}`) | |
); | |
}, | |
warn(t = '', ...args) { | |
if (DEBUG) | |
console.warn( | |
colors.yellow(`! ${t} ${args.map((a) => JSON.stringify(a)).join('\t')}`) | |
); | |
}, | |
error(t = '', ...args) { | |
if (DEBUG) | |
console.error( | |
colors.red(`* ${t} ${args.map((a) => JSON.stringify(a)).join('\t')}`) | |
); | |
}, | |
}; | |
const heartbeat = Buffer.from([ | |
0x00, 0x00, 0x00, 0x1f, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, | |
0x00, 0x00, 0x00, | |
]); | |
const colorList = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan'].reduce( | |
(acc, cur) => { | |
const [f, ...r] = cur; | |
return [...acc, cur, `bright${f.toUpperCase()}${r.join('')}`]; | |
}, | |
[] | |
); | |
const colorMap = new Map(); | |
function handleChunk(chunk = {}) { | |
const { cmd, data, info } = chunk; | |
if (cmd === 'DANMU_MSG') { | |
const user = info[2]; | |
const badge = info[3]; | |
const userId = user[0]; | |
let color; | |
if (colorMap.has(userId)) { | |
color = colorMap.get(userId); | |
} else { | |
color = colorList[~~(Math.random() * colorList.length)]; | |
colorMap.set(userId, color); | |
} | |
console.log( | |
colors.bold[color]( | |
`${badge[0] ? `[${badge[1]}(${badge[0]})] ` : ''}${user[1]}: ${info[1]}` | |
) | |
); | |
} else { | |
logger.log(cmd, data); | |
} | |
} | |
/** @param {Buffer} msg */ | |
function handleMessage(msg) { | |
const packetLen = msg.readUInt32BE(0); | |
msg = msg.slice(0, packetLen); | |
const headerLen = msg.readUInt16BE(4); | |
const _h = msg.slice(0, headerLen); | |
let body = msg.slice(headerLen); | |
const ver = _h.readUInt16BE(6); | |
const op = _h.readUInt32BE(8); | |
if (ver === 2) { | |
body = inflateSync(body); | |
const b = []; | |
for (let offset = 0; offset + 1 < body.length; ) { | |
const pLen = body.readUInt32BE(offset); | |
const item = body.slice(offset, offset + pLen); | |
b.push(item.slice(item.readUInt16BE(4))); | |
offset += pLen; | |
} | |
body = b; | |
} | |
if (op === 5) { | |
if (Array.isArray(body)) { | |
body.forEach((b) => { | |
try { | |
handleChunk(JSON.parse(b.toString())); | |
} catch (e) {} | |
}); | |
} else { | |
try { | |
handleChunk(JSON.parse(body.toString())); | |
} catch (e) {} | |
} | |
} | |
} | |
(async () => { | |
let _; | |
try { | |
_ = await getDanmuInfo(); | |
} catch (e) { | |
return exitWithErr('api fetch - network failed', e); | |
} | |
const { token = '', host_list: hostList = [] } = _; | |
if (!hostList.length) return exitWithErr('api fetch - ws host list empty'); | |
const [{ host }] = hostList; | |
const ws = new WebSocket(`wss://${host}/sub`); | |
ws.on('open', () => { | |
logger.warn('open'); | |
const d = JSON.stringify({ | |
uid: 0, | |
roomid: +roomId, | |
protover: 2, | |
platform: 'web', | |
type: 2, | |
key: token, | |
}); | |
const h = Buffer.from([ | |
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, | |
0x00, 0x00, 0x00, 0x00, | |
]); | |
h.writeUInt32BE(d.length + h.length, 0); | |
ws.send(Buffer.from([...h, ...new TextEncoder().encode(d)])); | |
setInterval(() => { | |
ws.send(heartbeat); | |
}, 30000); | |
}); | |
ws.on('error', (err) => { | |
logger.error('error', err); | |
}); | |
ws.on('close', (code, reason) => { | |
logger.warn('close', code, reason); | |
}); | |
ws.on('message', (msg) => { | |
logger.log('message'); | |
handleMessage(msg); | |
}); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "dc", | |
"version": "1.0.0", | |
"main": "dc.js", | |
"license": "MIT", | |
"dependencies": { | |
"axios": "^0.21.1", | |
"cheerio": "^1.0.0-rc.10", | |
"colors": "^1.4.0", | |
"ws": "^7.5.3" | |
}, | |
"devDependencies": {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment