├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app.js ├── bili-live.js ├── bin └── bilicom.js ├── commentclient.js ├── commentsend.js ├── libaosd.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # config file 31 | config.js 32 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztuowen/bilicom/05bd1f1b99eb34c13958307b2594fb707948f979/.gitmodules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Original work Copyright (c) 2014 payne 4 | Modified work Copyright (c) 2015 Tuowen Zhao 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bilicom Bilibili 直播弹幕助手 2 | 3 | ***This is based on the [original project](https://coding.net/u/payne/p/bili-comment/git) by payne*** 4 | 5 | `Bilibili 直播弹幕助手` 是一个帮助播主快速查看直播弹幕以及摆脱Flash观看B站直播的软件。 6 | 7 | 有几个功能: 8 | 9 | * 同步连接B站弹幕服务器 10 | * 断线重连 11 | * 弹幕通过aosd在屏幕上显示 12 | * 保存弹幕 13 | * 发送弹幕(使用cookie或者登录) 14 | * 保存密码加密后的cookie-file,不用重复登录/使用cookie 15 | * 观看B站直播视频流 16 | 17 | ## 安装与使用 18 | 19 | ### 安装 20 | 21 | 1. 安装必须的软件: nodejs 22 | 2. 安装可选软件: mpv, [libaosd(aosd_cat)](https://github.com/mkoskar/libaosd-xinerama) 23 | * mpv - 观看直播视频 24 | * libaosd - 屏幕上显示弹幕 25 | 2. 安装本弹幕助手: `sudo npm install -g bilicom` 26 | 27 | [***如何安装libaosd***](https://github.com/ztuowen/bilicom/issues/2) 28 | 29 | ### 使用 30 | 31 | 安装完成后运行`bilicom <直播间id>` 32 | 33 | 命令行参数: 34 | 35 | * `-c`或`--cookie` \ : 设置cookie 36 | * `-C`或`--cookie-file` \ : 设置cookie文件名 37 | * `-l`或`--log` : 保存弹幕 38 | * `-d`或`--dir` \ : 设置弹幕保存目录 39 | * `-L`或`--login` : 登录B站 40 | * `-p`或`--picexec` \ : 查看验证码的程序(\ \可以看图片的那种) 41 | 42 | 快捷键| 效果 43 | -----| --------------- 44 | q | 退出程序 45 | m | 打开播放器看直播 46 | p | 调整弹幕提示位置 47 | +/- | 增加减小提示字体 48 | t | 是否显示弹幕时间 49 | u | 是否显示弹幕发送者 50 | w | 是否显示欢迎信息 51 | n | 是否显示弹幕提示 52 | 回车| *发送弹幕* 53 | D | (发送测试弹幕) 54 | 55 | ### 发送弹幕 56 | 57 | ***使用Cookie发弹幕*** 58 | 59 | 1. 首先需要导出登录cookie 60 | 1. 在chrome中登录B站后 61 | 2. 打开开发者工具 62 | 3. 输入`document.cookie` 63 | 4. 拷贝返回的字符串(含引号) 64 | 2. 使用`bilicom <直播间号> -c <字符串>`运行程序 65 | 4. 回车键进入弹幕发送模式 66 | 5. 回车发送弹幕(空弹幕不会发送) 67 | 68 | ***B站登录发弹幕*** 69 | 70 | 1. 使用`bilicom <直播间号> -L`运行程序 71 | 2. 输入用户名和密码 72 | 3. 验证码有的时候可以不用输(少年,赌一把吧~) 73 | 4. 登录成功?发弹幕吧 74 | 75 | ***Cookie file*** 76 | 77 | 1. **`-C `指定文件,即可保存cookie(会询问加密密码)** 78 | 2. 使用时需指定`-C ` 指定cookie-file 79 | 3. 解密后就可以发弹幕了 80 | 81 | ## 没图说个XX 82 | 83 | ### 弹幕机界面 84 | 85 | ![image](https://cloud.githubusercontent.com/assets/6838440/12380470/09ffb494-bd31-11e5-8d4d-78a9624799aa.png) 86 | 87 | ### 多个弹幕的叠加效果 88 | 89 | ![image](https://cloud.githubusercontent.com/assets/6838440/12380496/7a9eaaca-bd31-11e5-96e8-85a128e11a93.png) 90 | 91 | ### 使用弹幕机观看直播 92 | 93 | ![image](https://cloud.githubusercontent.com/assets/6838440/12380533/50ecedc6-bd32-11e5-8982-a329838650b6.png) 94 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var app = function(){ 2 | var fs = require('fs'); 3 | var logStream; 4 | var colors = require('colors'); 5 | var libnotify = require('./libaosd.js'); 6 | var pkginfo = require('./package.json'); 7 | var request = require('request').defaults({jar: true}); 8 | 9 | var CommentClient = require('./commentclient.js').Client; 10 | var Bili_live = require('./bili-live.js'); 11 | var liveid; 12 | var config={ 13 | "showTime":['t',false,'发射时间'], 14 | "showUserName":['u',true,'弹幕发送者'], 15 | "showWelcome":['w',true,'欢迎信息'], 16 | "notify":['n',false,'弹幕提示'], 17 | }; 18 | var notify=true; 19 | var child_process = require('child_process'); 20 | var comsend=null; 21 | 22 | var blessed = require('blessed'); 23 | var screen,cmtBox,liveid,viewNum; 24 | var intervals=[]; 25 | var postag=[['topleft','上左'],['topmid','上中'],['topright','上右'],['botleft','下左'],['botmid','下中'],['botright','下右']]; 26 | var curpos=0; 27 | var notconf={loc:postag[curpos][0], 28 | size:16 29 | }; 30 | var cwd; 31 | var footer; 32 | var inbox; 33 | 34 | var nowclient; 35 | // The theme definition is taken from htop 36 | // TODO need to adapt it to this program 37 | var theme = { 38 | "name": "Becca", 39 | "author": "James Hall", 40 | "description": "In memory of Becca #663399. This is as close as we can get to that color in xterm", 41 | "title": { 42 | "fg": "#800080" 43 | }, 44 | "chart": { 45 | "fg": "#800080", 46 | "border": { 47 | "type": "line", 48 | "fg": "#800080" 49 | } 50 | }, 51 | "table": { 52 | "fg": "white", 53 | "items": { 54 | "selected": { 55 | "bg": "#800080", 56 | "fg": "bg" 57 | }, 58 | "item": { 59 | "fg": "fg", 60 | "bg": "bg" 61 | } 62 | }, 63 | "border": { 64 | "type": "line", 65 | "fg": "#800080" 66 | } 67 | }, 68 | "footer": { 69 | "fg": "fg" 70 | } 71 | }; 72 | 73 | function drawHeader(liveid){ 74 | var headerText, headerTextNoTags; 75 | headerText = ' {bold}Bilicom{/bold}{white-fg} '+ pkginfo.version + ' for ' + liveid + ' '; 76 | headerTextNoTags = ' Bilicom '+ pkginfo.version + ' for ' + liveid + ' '; 77 | 78 | var header = blessed.text({ 79 | top: 'top', 80 | left: 'left', 81 | width: headerTextNoTags.length, 82 | height: '1', 83 | fg: theme.title.fg, 84 | content: headerText, 85 | tags: true 86 | }); 87 | 88 | viewNum = blessed.text({ 89 | top: 'top', 90 | right: 0, 91 | width: 15, 92 | height: '1', 93 | align: 'right', 94 | content: '', 95 | tags: true 96 | }); 97 | screen.append(header); 98 | screen.append(viewNum); 99 | } 100 | function drawFooter(){ 101 | inbox = blessed.textarea({ 102 | bottom: 1, 103 | height: 1, 104 | left: '0%', 105 | width: '100%', 106 | inputOnFocus: true, 107 | style: { 108 | fg: '#787878', 109 | bg: '#454545', 110 | 111 | focus: { 112 | fg: '#f6f6f6', 113 | bg: '#353535' 114 | } 115 | } 116 | }) 117 | footer = blessed.text({ 118 | bottom: '0', 119 | left: '0%', 120 | width: '100%', 121 | align: 'right', 122 | tags:true, 123 | content: '', 124 | fg: theme.footer.fg 125 | }); 126 | inbox.key('enter',function(ch,key){ 127 | var message = this.getValue().replace(/(\r\n|\n|\r)/gm,""); 128 | this.clearValue(); 129 | if (comsend && message.length > 0) 130 | { 131 | comsend.send(message,function (errmsg){ 132 | cmtBox.insertLine(0,"[系统] ".bold.red+("弹幕发送失败:"+errmsg).bold); 133 | }); 134 | } 135 | screen.rewindFocus(); 136 | screen.render(); 137 | }); 138 | updateFooter(); 139 | screen.append(footer); 140 | screen.append(inbox); 141 | } 142 | 143 | function updateFooter(){ 144 | var text = ''; 145 | for (var c in config) { 146 | var command = config[c]; 147 | if (command[1]) 148 | text += ' {white-bg}{black-fg}' + command[0] + '{/black-fg}{/white-bg}' + scrmul(command[2]); 149 | else 150 | text += ' {white-fg}{black-bg}' + command[0] + '{/black-bg}{/white-fg}' + scrmul(command[2]); 151 | } 152 | footer.setContent(' {white-fg}{black-bg}q{/black-bg}{/white-fg}'+scrmul('退出') 153 | +' {white-fg}{black-bg}m{/black-bg}{/white-fg}'+scrmul('播放器') 154 | +' {white-fg}{black-bg}p{/black-bg}{/white-fg}'+scrmul(postag[curpos][1]) 155 | +' {white-fg}{black-bg}+/-{/black-bg}{/white-fg}'+scrmul(notconf.size)+text); 156 | function scrmul(str){ 157 | if (screen.width>90) 158 | return ' '+str; 159 | return ''; 160 | } 161 | } 162 | 163 | return { 164 | init: function() { 165 | // parse cmdline 166 | var argv = require('yargs') 167 | .usage('usage: $0 [liveid] ') 168 | .demand(1) 169 | .boolean('L','login') 170 | .describe('L','Bilibili login') 171 | .alias('c', 'cookie') 172 | .describe('c', 'use cookie string') 173 | .alias('C', 'cookie-file') 174 | .describe('C', 'use cookie file(load/store,encrypted)') 175 | .alias('p', 'picexec') 176 | .describe('p', 'picture viewer(default:gpicview)') 177 | .boolean('l','log') 178 | .describe('l','enable logging') 179 | .alias('d','dir') 180 | .describe('d', 'log file dir,default to current dir') 181 | .help('help') 182 | .argv; 183 | liveid=argv._[0]; 184 | Bili_live.getRoomID(liveid,afterRID); 185 | function afterRID(rid) 186 | { 187 | liveid=rid; 188 | if (argv.d) 189 | cwd=argv.d; 190 | else 191 | cwd=process.cwd(); 192 | var wOption = { 193 | flags: 'a', 194 | encoding: null, 195 | mode: '0666' 196 | }; 197 | 198 | if (argv.l) 199 | { 200 | logStream = fs.createWriteStream(cwd+'/'+liveid+'_'+new Date().getTime()+'.source',wOption); 201 | var liveinfo = {liveid: liveid}; 202 | logStream.write(new Buffer(JSON.stringify(liveinfo))); 203 | logStream.write(new Buffer([0x00,0x00])); 204 | } 205 | if (argv.L) { 206 | require('./commentsend.js').login().login(null,null,afterLogin,argv.p); 207 | } 208 | else 209 | afterLogin(argv.c); 210 | } 211 | function afterLogin(cookie) 212 | { 213 | var fname = null; 214 | if (argv.C) fname = argv.C; 215 | if (cookie) 216 | { 217 | comsend=require('./commentsend.js').comsend(); 218 | comsend.initUnenc(fname,cookie,liveid,initBlessed); 219 | } 220 | else { 221 | var st; 222 | try{ 223 | st=fs.statSync(fname); 224 | if (st.isFile()) 225 | { 226 | comsend=require('./commentsend.js').comsend(); 227 | comsend.init(fname,liveid,initBlessed); 228 | } 229 | else 230 | initBlessed(); 231 | } catch (e) { 232 | initBlessed(); 233 | } 234 | } 235 | } 236 | } 237 | } 238 | function initBlessed() 239 | { 240 | // Clear all stdin listeners 241 | process.stdin.removeAllListeners('keypress'); 242 | if (!process.version.match(/^v0/)) 243 | process.stdin.removeAllListeners('data'); 244 | 245 | // Create a screen object 246 | screen = blessed.screen({ 247 | terminal: 'xterm-256color', 248 | fullUnicode:true 249 | }); 250 | 251 | //Checking key strokes 252 | screen.on('keypress', function(ch, key){ 253 | switch (ch){ 254 | case 'q': 255 | process.exit(0); 256 | break; 257 | case 'm': 258 | runmpv(); 259 | break; 260 | case 'n': 261 | if (notify) 262 | config.notify[1] = !config.notify[1]; 263 | break; 264 | case 't': 265 | config.showTime[1] = !config.showTime[1]; 266 | break; 267 | case 'u': 268 | config.showUserName[1] = !config.showUserName[1]; 269 | break; 270 | case 'w': 271 | config.showWelcome[1]= !config.showWelcome[1]; 272 | break; 273 | case 'p': 274 | notconf.loc=postag[curpos=(curpos+1)%6][0]; 275 | break; 276 | case 'D': 277 | libnotify.notify("[测试] 这只是一个测试",notconf); 278 | break; 279 | case '+': 280 | if (notconf.size<30) 281 | ++notconf.size; 282 | break; 283 | case '-': 284 | if (notconf.size>10) 285 | --notconf.size; 286 | break; 287 | default: 288 | } 289 | updateFooter(); 290 | }); 291 | screen.key('enter',function(ch,key){ 292 | if (comsend) 293 | inbox.focus(); 294 | else 295 | { 296 | inbox.content="如要发送弹幕,请参考'bilicom --help'"; 297 | screen.render(); 298 | } 299 | }); 300 | 301 | drawHeader(liveid); 302 | drawFooter(); 303 | 304 | cmtBox = blessed.box({ 305 | top: 1, 306 | left: 'left', 307 | width: '100%', 308 | height: screen.height-3, 309 | keys: true, 310 | mouse: true, 311 | scrollable: true, 312 | fg: theme.table.fg 313 | }); 314 | screen.append(cmtBox); 315 | 316 | screen.render(); 317 | 318 | var setupCharts = function() { 319 | cmtBox.height = screen.height-3; 320 | updateFooter(); 321 | }; 322 | 323 | screen.on('resize', setupCharts); 324 | intervals.push(setInterval(draw, 100)); 325 | 326 | function draw() 327 | { 328 | screen.render(); 329 | } 330 | /** 331 | * Init Chat Client 332 | */ 333 | (function(chat_id){ 334 | cmtBox.insertLine(0,("=========直播间信息=========\nchat_id : " + chat_id.toString() + "\n============================").cyan); 335 | 336 | setupClient(chat_id); 337 | }(liveid)); 338 | libnotify.callnotify("",{help:true},function(){ 339 | cmtBox.insertLine(0,"[系统] ".red.bold+"aosd_cat没有找到,请验证libaosd安装是否成功".bold); 340 | notify =false; 341 | }); 342 | /** 343 | * Open media player 344 | */ 345 | function runmpv(){ 346 | Bili_live.getLiveUrls(liveid, function(err,url){ 347 | if (err==null) { 348 | var child = child_process.spawn('mpv',['-v'],{detached:true,stdio: [ 'ignore', 'ignore', 'ignore' ]}); 349 | child.on('error',function(err){ 350 | cmtBox.insertLine(0,"[系统] ".red.bold+"mpv没有找到,请验证mpv安装是否成功".bold); 351 | }); 352 | child.on('close',function(code){ 353 | if (code) 354 | return; 355 | cmtBox.insertLine(0,"[系统] ".bold.red+"正在启动播放器".bold); 356 | var child = child_process.spawn('mpv',[url],{detached:true, stdio: [ 'ignore', 'ignore', 'ignore' ]}); 357 | child.unref(); 358 | }); 359 | } 360 | }); 361 | } 362 | }; 363 | function setupClient(cid) { 364 | Bili_live.getChatServer(cid, function(host) { 365 | nowclient = connectCommentServerWithHost(cid,host); 366 | }) 367 | } 368 | /** 369 | * 连接弹幕服务器 370 | * @param cid 371 | * @param host 372 | * @returns {*|Client} 373 | */ 374 | function connectCommentServerWithHost(cid, host){ 375 | var server= new CommentClient({host: host, port: 788}); 376 | 377 | server.on('server_error', function(err) { 378 | if (err.code) 379 | cmtBox.insertLine(0,"[系统] ".red.bold+("与服务器链接中断: "+err.code).bold); 380 | else 381 | cmtBox.insertLine(0,"[系统] ".bold.red + ("服务器发生错误: " + err).bold); 382 | }); 383 | server.on('close', function() { 384 | cmtBox.insertLine(0,"[系统] ".bold.red + "5s后重新建立链接".bold); 385 | setTimeout(function(){ 386 | setupClient(liveid); 387 | }, 5000); 388 | }); 389 | server.on('error', function(error) { 390 | cmtBox.insertLine(0,"[系统] ".bold.red + ("发生错误:" + error).bold); 391 | }); 392 | server.on('login_success', function(num) { 393 | viewNum.setContent("在线人数 " + num.toString()); 394 | if(logStream){ 395 | logStream.write(new Buffer(JSON.stringify({action:"watcherNum",num:num}))); 396 | logStream.write(new Buffer([0x00])); 397 | } 398 | }); 399 | server.on('newCommentString', function(data) { 400 | data = JSON.parse(data); 401 | //save Danmu Info 402 | if(logStream){ 403 | logStream.write(new Buffer(JSON.stringify(data))); 404 | logStream.write(new Buffer([0x00])); 405 | } 406 | 407 | if(!data && !data.roomid) { 408 | cmtBox.insertLine(0,JSON.stringify(data,null,2)); 409 | return cmtBox.insertLine(0,"[弹幕] ".bold.green + "异常数据".red); 410 | } 411 | 412 | switch (data.cmd) { 413 | case "SEND_GIFT": 414 | data=data.data; 415 | var text=''; 416 | var date = data.timestamp; 417 | date = DateFormat(date, 'hh:mm:ss');//yyyy-MM-dd 418 | if(config.showTime[1]) text += ('[' + date + '] ').toString().yellow; 419 | var username = selectColorText(data.uname,data.uid).bold; 420 | text += username + " " + colors.yellow(data.action).bold + " " + colors.red(data.giftName + "x" + data.num).bold; 421 | cmtBox.insertLine(0,"[投喂] ".bold.yellow + text); 422 | if (config.notify[1]) 423 | { 424 | text = "[投喂] " + data.uname + " " + data.action + " " + data.giftName + "x" + data.num; 425 | libnotify.notify(text,notconf); 426 | } 427 | break; 428 | case "WELCOME": 429 | if (config.showWelcome[1]){ 430 | data=data.data; 431 | var text=''; 432 | text += colors.yellow("欢迎") + " " + colors.red(data.uname) + " " + colors.yellow("进入直播间"); 433 | cmtBox.insertLine(0,"[欢迎] ".bold.yellow + text); 434 | if (config.notify[1]) 435 | { 436 | text = "[欢迎] " + "老爷" + data.uname + "进入直播间"; 437 | libnotify.notify(text,notconf); 438 | } 439 | } 440 | break; 441 | case "DANMU_MSG": 442 | data = data.info;//ignore other arguments 443 | 444 | //获取时间 445 | var date = data[0][4]; 446 | var msg = data[1]; 447 | date = DateFormat(date, 'hh:mm:ss');//yyyy-MM-dd 448 | 449 | //获取发布者名称 450 | var username = ''; 451 | username = selectColorText(data[2][1],data[2][0]).bold + " "; 452 | if(data[3].length>0) { 453 | username = colors.blue("(" + data[3][1] + ")") + username; 454 | } 455 | 456 | var text=''; 457 | if(config.showTime[1]) text += ('[' + date + '] ').toString().yellow; 458 | if(config.showUserName[1]) text += username; 459 | text += replaceES(msg).bold; 460 | text = "[弹幕] ".bold.green + text; 461 | cmtBox.insertLine(0,text); 462 | 463 | if (config.notify[1]) 464 | { 465 | text=''; 466 | username = ''; 467 | if(data.length == 6){ 468 | username = data[2][1] + " "; 469 | } 470 | if(data[3].length>0) { 471 | username = "(" + data[3][1] + ")" + username; 472 | } 473 | if(config.showUserName[1]) text += username; 474 | text += msg; 475 | text = "[弹幕] " + text; 476 | 477 | libnotify.notify(text,notconf); 478 | } 479 | break; 480 | default: 481 | cmtBox.insertLine(0,JSON.stringify(data,null,2)); 482 | cmtBox.insertLine(0,"[弹幕] ".bold.green + "空弹幕".red); 483 | } 484 | 485 | }); 486 | server.on('newScrollMessage', function(data) { 487 | //json {text:"",highlight:?,bgcolor:?,flash:?,tooltip:?} 488 | cmtBox.insertLine(0,"新滚动信息:" + eval("("+data+")").text); 489 | }); 490 | 491 | server.on('unknown_bag', function(data) { 492 | cmtBox.insertLine(0,("异常数据:" + data).toString().red); 493 | }); 494 | server.connect(cid); 495 | return server; 496 | 497 | function selectColorText(text,id){ 498 | var _colors = ['yellow', 'red', 'green', 'cyan', 'magenta']; 499 | return colors[_colors[id % _colors.length]](text); 500 | } 501 | } 502 | 503 | /* 504 | UTIL 505 | */ 506 | function parseLiveUrl(url){ 507 | var liveid = (url + "/").match(/live.bilibili.com\/(.*?)\//); 508 | return liveid[1]; 509 | } 510 | 511 | function isblank(text){ 512 | return(!text || text==''); 513 | } 514 | function replaceES(text){ 515 | return html_decode(text); 516 | } 517 | function html_decode(str) 518 | { 519 | var s; 520 | if (str.length == 0) return ""; 521 | s = str.replace(/>/g, "&"); 522 | s = s.replace(/</g, "<"); 523 | s = s.replace(/>/g, ">"); 524 | s = s.replace(/ /g, " "); 525 | s = s.replace(/'/g, "\'"); 526 | s = s.replace(/"/g, "\""); 527 | s = s.replace(/
/g, "\n"); 528 | return s; 529 | } 530 | function DateFormat(time,fmt) { 531 | time=new Date(time * 1000); 532 | var o = { 533 | "M+": time.getMonth() + 1, //月份 534 | "d+": time.getDate(), //日 535 | "h+": time.getHours(), //小时 536 | "m+": time.getMinutes(), //分 537 | "s+": time.getSeconds(), //秒 538 | "q+": Math.floor((time.getMonth() + 3) / 3), //季度 539 | "S": time.getMilliseconds() //毫秒 540 | }; 541 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (time.getFullYear() + "").substr(4 - RegExp.$1.length)); 542 | for (var k in o) 543 | if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 544 | return fmt; 545 | } 546 | 547 | }(); 548 | 549 | app.init(); 550 | -------------------------------------------------------------------------------- /bili-live.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var xmlreader = require('xmlreader'); 3 | 4 | var lang={ 5 | a1: ' is empty', 6 | b1: 'failed to connect to server' 7 | }; 8 | 9 | exports.getRoomID = function(liveid,callback){ 10 | 11 | var options = { 12 | url: 'http://live.bilibili.com/'+liveid, 13 | gzip: true 14 | }; 15 | function rcallback(error, response, body) { 16 | if (!error && response.statusCode == 200) { 17 | var tmp,roomid; 18 | tmp = body.match(/ROOMID \= (.*?)\;/); 19 | if(tmp&&tmp.length>=2) roomid = tmp[1]; 20 | return callback(parseInt(roomid)); 21 | }else 22 | callback(liveid); 23 | } 24 | request(options, rcallback); 25 | }; 26 | 27 | exports.getChatServer = function(roomid, callback){ 28 | var options = { 29 | url: 'http://live.bilibili.com/api/player?id=cid:'+roomid, 30 | gzip: true 31 | }; 32 | function rcallback(error, response, body) { 33 | if (!error && response.statusCode == 200) { 34 | var tmp,server; 35 | tmp = body.match(/(.*?)<\/server>/); 36 | if (tmp && tmp.length>=2) server = tmp[1]; 37 | return callback(server); 38 | } else 39 | callback("livecmt-1.bilibili.com"); 40 | } 41 | request(options, rcallback); 42 | }; 43 | 44 | exports.getLiveInfo = function(liveid,callback){ 45 | if(!liveid) return callback("liveid"+lang.a1); 46 | 47 | var options = { 48 | url: 'http://live.bilibili.com/ajax/schedule/'+liveid, 49 | gzip: true 50 | }; 51 | 52 | function rcallback(error, response, body) { 53 | if (!error && response.statusCode == 200) { 54 | var info = JSON.parse(body); 55 | return callback(null, info); 56 | }else{ 57 | return callback(lang.b1); 58 | } 59 | } 60 | request(options, rcallback); 61 | }; 62 | 63 | exports.getPlayerInfo = function(cid,cookies,callback){ 64 | if(!cid) return callback("cid"+lang.a1); 65 | 66 | var options = { 67 | url: 'http://interface.bilibili.com/player?id=cid:'+cid, 68 | header:{ 69 | Cookie:cookies?cookies:"" 70 | }, 71 | gzip: true 72 | }; 73 | 74 | function rcallback(error, response, body) { 75 | if (!error && response.statusCode == 200) { 76 | xmlreader.read(''+body+'', function (err, res){ 77 | if(err) return callback(err); 78 | return callback(null,res.data); 79 | }); 80 | }else{ 81 | return callback(lang.b1); 82 | } 83 | } 84 | request(options, rcallback); 85 | }; 86 | 87 | exports.getLiveUrls = function(liveid,callback){ 88 | if(!liveid) return callback("liveid"+lang.a1); 89 | 90 | var options = { 91 | url: 'http://live.bilibili.com/api/playurl?cid='+liveid, 92 | gzip: true 93 | }; 94 | 95 | function rcallback(error, response, body) { 96 | if (!error && response.statusCode ==200) { 97 | xmlreader.read(body, function (err,res) { 98 | if (err) return callback(err); 99 | return callback(null,res.video.durl.b1url.text()); 100 | }); 101 | } else { 102 | return callback(lang.b1); 103 | } 104 | } 105 | request(options,rcallback); 106 | } 107 | -------------------------------------------------------------------------------- /bin/bilicom.js: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ':' //; # This line below fixes xterm color bug on Mac - https://github.com/MrRio/vtop/issues/2 3 | ':' //; export TERM=xterm-256color 4 | ':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@" 5 | 6 | require('../app.js'); -------------------------------------------------------------------------------- /commentclient.js: -------------------------------------------------------------------------------- 1 | /** 必要的node.js库**/ 2 | var net = require('net'); 3 | var events = require('events'), 4 | util = require('util'); 5 | //init 6 | 7 | function Client(base) { 8 | var self = this; 9 | events.EventEmitter.call(this); 10 | this.base = base; 11 | this.state = 0; //0 未连接 1 待命 2 数据接收未完成 12 | this.timer; 13 | this.client = new net.Socket(); 14 | this.client.setEncoding('binary'); 15 | this.client.on('data', function(data) { 16 | var result, bdata = new Buffer(data, "binary"); 17 | if(bdata.length>=1){ 18 | if(self.state == 1) { //可以开始接收数据了 19 | var parser_length=bdata.readUInt32BE(0); 20 | 21 | this.buffer_data = new Buffer(0); 22 | this.buffer_length=parser_length; 23 | } 24 | this.buffer_data=Buffer.concat([this.buffer_data,bdata]); 25 | if(this.buffer_length >= this.buffer_data.length){ //接收完毕 26 | self.state = 1; 27 | if(this.buffer_length == this.buffer_data.length){ 28 | self.deliverData(this.buffer_data); 29 | }else{ 30 | self.emit('unknown_bag', this.buffer_data); 31 | } 32 | return; 33 | } 34 | //未接收完毕的继续 35 | } 36 | }); 37 | this.client.on('error', function(error) { 38 | self.state = 1; 39 | clearTimeout(this.timer); 40 | self.timer=null; 41 | console.log('server_error'+error); 42 | self.emit('server_error', error); 43 | }); 44 | this.client.on('close', function() { 45 | self.state = 0; 46 | clearTimeout(self.timer); 47 | self.timer=null; 48 | console.log('close'); 49 | self.emit('close'); 50 | }); 51 | } 52 | util.inherits(Client, events.EventEmitter); 53 | 54 | /** 55 | * Connect to Chat Server 56 | * @param chatid 57 | * @param userid 58 | * @param pwd 59 | */ 60 | Client.prototype.connect = function(chatid, userid) { 61 | var self = this; 62 | if (this.state != 0) return; 63 | this.client.connect(self.base.port, self.base.host, function(err) { 64 | if (err) 65 | console.log(err); 66 | if(!userid) userid=Math.floor(1e14+Math.random()*2e14); 67 | var packetModel = {roomid: chatid, uid:userid}; 68 | var data=pack_data(7,JSON.stringify(packetModel)); 69 | self.send(data); 70 | self.state = 1; 71 | }); 72 | }; 73 | /** 74 | * Directly Send Message to Chat Server 75 | * @param data 76 | * @returns {boolean} 77 | */ 78 | Client.prototype.send = function(data) { 79 | if(this.client.write(data)){ 80 | this.state = 1; 81 | return true; 82 | }else{ 83 | return false; 84 | } 85 | }; 86 | /** 87 | * Disconnect 88 | */ 89 | Client.prototype.disconnect = function() { 90 | this.client.destory(); 91 | }; 92 | /** 93 | * 94 | * @param data 95 | * @returns {*} 96 | */ 97 | Client.prototype.deliverData = function (data){ 98 | var self = this; 99 | if(data.length < 2) return this.emit('error', '意外的数据包'); 100 | 101 | var index = data.readUInt32BE(8); 102 | 103 | switch(index){ 104 | case 3: 105 | this.emit('login_success', data.readUInt32BE(16)); 106 | break; 107 | case 8: 108 | if(!this.timer){ 109 | this.timer=setInterval(function() { 110 | var data = pack_data(2,""); 111 | self.send(data); 112 | }, 2*1000); 113 | } 114 | break; 115 | case 5: 116 | var jsonData = data.slice(16); 117 | this.emit('newCommentString', jsonData.toString('utf8')); 118 | break; 119 | case 6: 120 | var jsonData = data.slice(16); 121 | this.emit('newScrollMessage', jsonData.toString('utf8')); 122 | break; 123 | case 17: 124 | break; 125 | } 126 | 127 | }; 128 | 129 | function pack_data(action, payload) { 130 | return pack_data_more(16,1,action,1,payload); 131 | } 132 | 133 | function pack_data_more(magic, ver, action, param, body) { 134 | var bufferdata = new Buffer(16+body.length); 135 | bufferdata.writeUInt32BE(16+body.length, 0); 136 | bufferdata.writeUInt16BE(magic, 4); 137 | bufferdata.writeUInt16BE(ver, 6); 138 | bufferdata.writeUInt32BE(action, 8); 139 | bufferdata.writeUInt32BE(param, 12); 140 | bufferdata.write(body, 16); 141 | return bufferdata; 142 | } 143 | 144 | exports.Client = Client; 145 | 146 | //cli = new Client({host: 'livecmt-2.bilibili.com', port: 788}); 147 | //cli.connect(53714); 148 | 149 | //(function trap(){ 150 | // setTimeout(trap,3000); 151 | //})(); 152 | -------------------------------------------------------------------------------- /commentsend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Using cookie to send comments to Bilibili 3 | **/ 4 | 5 | var crypto = require('crypto-js'); 6 | var fs = require('fs'); 7 | var read = require('read'); 8 | var NodeRSA = require('node-rsa'); 9 | var child_process = require("child_process"); 10 | var os = require("os"); 11 | 12 | // TODO needs a better way of defining a class than using closure 13 | exports.comsend = function (){ 14 | var cookie = ""; 15 | var request = require('request').defaults({jar: true}); 16 | var rnd; 17 | var rid; 18 | return { 19 | // Init with non-encrypted cookie 20 | // Will encrypt&store cookie to cookie-file if fname is given 21 | initUnenc: function(fname,ck,roomid,callback){ 22 | rnd=Math.round((new Date).getTime()/1000); 23 | cookie = ck; 24 | rid = roomid; 25 | if (fname) 26 | { 27 | read({prompt:'设置cookie保存文件的密码? ',silent:true,replace:'*',terminal:true}, function (err,passwd) { 28 | var enc=crypto.AES.encrypt(ck,passwd).toString(); 29 | fs.writeFile(fname,enc); 30 | callback(); 31 | }); 32 | } 33 | else 34 | callback(); 35 | }, 36 | // Init with a cookie file 37 | init: function(fname,roomid,callback){ 38 | rid = roomid; 39 | rnd=Math.round((new Date).getTime()/1000); 40 | 41 | read({prompt:'请输入cookie保存文件的密码? ',silent:true,replace:'*',terminal:true}, function (err,passwd) { 42 | fs.readFile(fname,function (err,data) { 43 | var dec = crypto.AES.decrypt(data.toString(),passwd); 44 | cookie = dec.toString(crypto.enc.Utf8); 45 | callback(); 46 | }); 47 | }); 48 | 49 | }, 50 | // Send messages to bilibili comments server 51 | send: function(msg,errcbk){ 52 | var form={color:16777215, 53 | fontsize:25, 54 | mode:1, 55 | msg:msg, 56 | rnd:rnd, 57 | roomid:rid}; 58 | request.post({url:'http://live.bilibili.com/msg/send', 59 | headers:{ 60 | cookie:cookie, 61 | Host: 'live.bilibili.com', 62 | Origin: 'http://live.bilibili.com', 63 | Referer: 'http://live.bilibili.com/', 64 | }},function(err,response,data) 65 | { 66 | if (err) 67 | errcbk("网络连接出错"); 68 | else 69 | { 70 | data = JSON.parse(data); 71 | if (data.code) 72 | errcbk(data.msg); 73 | } 74 | }).form(form); 75 | } 76 | } 77 | }; 78 | 79 | var baseurl = 'https://passport.bilibili.com'; 80 | const captname = os.tmpdir()+"/captcha.png"; 81 | 82 | exports.login = function (){ 83 | var request = require('request').defaults({jar: true}); 84 | var cookiejar = request.jar(); 85 | var rnd=Math.round((new Date).getTime()/1000); 86 | var loggedin =false; 87 | return { 88 | login: function (uname,passwd,callback,picexec){ 89 | var captcha; 90 | var fts; 91 | var picexec = picexec || "gpicview"; 92 | getCaptcha(); 93 | function getCaptcha(){ 94 | request({url:baseurl+"/captcha", 95 | jar:cookiejar, 96 | headers: { 97 | Referer: 'https://passport.bilibili.com/ajax/miniLogin/minilogin' 98 | }},getLoginKey).pipe(fs.createWriteStream(captname)); 99 | } 100 | function getLoginKey(err,response,body) { 101 | if (!uname) 102 | read({prompt:'用户名? ',terminal:true}, function (err,answer) { 103 | uname = answer; 104 | readPasswd(); 105 | }); 106 | else 107 | readPasswd(); 108 | function readPasswd() { 109 | if (!passwd) 110 | read({prompt:'密码? ',silent:true,replace:'*',terminal:true},function (err,answer) { 111 | passwd = answer; 112 | readCaptcha(); 113 | }); 114 | else 115 | readCaptcha(); 116 | } 117 | function readCaptcha() { 118 | var captview = child_process.spawn(picexec,[captname]); 119 | 120 | captview.on('error',function(){ 121 | console.log("使用"+picexec+"打开验证码失败,请手动打开"+captname+"查看"); 122 | }); 123 | read({prompt:'验证码? ',terminal:true},function (err,answer) { 124 | try{ 125 | captview.kill(); 126 | }catch(e){} 127 | fs.unlink(captname); 128 | 129 | captcha = answer; 130 | 131 | request({method:'GET', 132 | url:baseurl+"/login?act=getkey&_="+(new Date).getTime(), 133 | gzip: true, 134 | jar:cookiejar, 135 | headers: { 136 | Referer: 'https://passport.bilibili.com/ajax/miniLogin/minilogin' 137 | } 138 | },encodeLoginInfo); 139 | }); 140 | } 141 | } 142 | function encodeLoginInfo(err,response,body){ 143 | if (err==null) 144 | { 145 | body=JSON.parse(body); 146 | var key = new NodeRSA(body.key); 147 | key.setOptions({encryptionScheme:'pkcs1'}); 148 | var enpasswd = key.encrypt(body.hash+passwd,'base64','utf8'); 149 | var form = {userid:uname,pwd:enpasswd,captcha:captcha,keep:1}; 150 | request.post({ 151 | url:baseurl+"/ajax/miniLogin/login", 152 | gzip: true, 153 | jar: cookiejar, 154 | headers: { 155 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 156 | 'User-Agent':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36', 157 | Host: 'passport.bilibili.com', 158 | Origin: 'https://passport.bilibili.com', 159 | Referer: 'https://passport.bilibili.com/ajax/miniLogin/minilogin' 160 | }, 161 | },checkLoginRet).form(form); 162 | } 163 | } 164 | function checkLoginRet(err,response,body) { 165 | body = JSON.parse(body); 166 | if (!body.status) 167 | { 168 | console.log("登录失败!"); 169 | if (body.message.code==-105) 170 | console.log("要输验证码咯~"); 171 | getCaptcha(); 172 | } 173 | else 174 | { 175 | loggedin =true; 176 | callback(cookiejar.getCookieString('http://www.bilibili.com')); 177 | } 178 | } 179 | }, 180 | }; 181 | } 182 | -------------------------------------------------------------------------------- /libaosd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple notification library using aosd_cat 3 | **/ 4 | 5 | var child_process = require('child_process'); 6 | 7 | // Timeout of notification to be 4s 8 | var timeout=4000; 9 | 10 | // Each position has its own queue in case of conflict 11 | var position={topleft:['0',1,0,[]], 12 | topmid:['1',1,0,[]], 13 | topright:['2',1,0,[]], 14 | botleft:['6',-1,0,[]], 15 | botmid:['7',-1,0,[]], 16 | botright:['8',-1,0,[]] 17 | }; 18 | 19 | // Wraper for raw aosd_cat 20 | // Input: msg & options@{size,loc} 21 | exports.notify = function(msg,options) { 22 | var date = new Date(); 23 | var time= date.getTime(); 24 | var options=options || {}; 25 | options.size = options.size || 16; 26 | options.loc = options.loc || 'topleft'; 27 | 28 | var pos = getempty(options.loc); 29 | position[options.loc][3][pos]=time; 30 | callnotify(msg,{time:timeout, 31 | offset:Math.round(pos*(options.size+1)*1.7*position[options.loc][1])+'', 32 | fontsize:options.size, 33 | color:'white', 34 | position:position[options.loc][0] 35 | }); 36 | 37 | // Find the first empty place 38 | function getempty(loc){ 39 | for (var i=0;i