bilibili视频下载 (node.js)

3 篇文章 0 订阅
2 篇文章 0 订阅

前言

  1. SESSDATA 后面的 ; 分号必需写上,否则请求失败或报错。
  2. 后面的实现主要是练习下 js 的特性,使用时看接口就好。
  3. node的控制台刷新显示不清楚,将就下默认的计时器显示。这里设置了回调函数,网页热更新的话,应该可以在相应的位置显示进度。
  4. 希望各界精英可以留下宝贵的意见,谢谢。

源码

bilibili.js

const axios = require('axios');
const fs = require('fs');
const path = require('path');
const cmd = require('node-cmd');
const process = require('process');
const events = require('events');

(function (g, x, r, h, d, m) {
	let e = this
	g.req = r
    g.reqh = h
    g.downl = d
    g.bili = m
	x = g
	m[1](r, h, d)
})(this, module.exports, function (args, resolve, method='get') {
	let obj = {'method': method}
	if (typeof args === 'object')
		for (let k in args)
	    	obj[k] = args[k]
	axios(obj).then(function (res_json) {
		resolve(res_json)
	}).catch(function (error) {
		console.log(error)
	})
}, function (args=false) {
	headers = {"user-agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36 "}
    if (typeof args === 'object')
    	for (let k in args)
    		headers[k] = args[k]
    return headers
}, function (u, h, p, r, l) {
	return new Promise ((res, err) => {
		r({'url': u, 'headers': h, responseType: 'stream'}, d => {
			let os = d.headers['content-length'],  
			e = fs.existsSync(p), 
			es = 0,
			itv = 5000,
			m =  'w', w, s, t, o
			if (e) {s = fs.statSync(p); if (s.isFile) es = s.size}
			if (es == os) {res(true); return}
			else if (es > os) fs.unlink(p, function(err) {if(err) throw err})
			else {m = 'a'; h.range = `bytes=${es}-`}
			w = fs.createWriteStream(p, {flags: m, highWaterMark: 3})
			d.data.pipe(w)
			o = () => {l(fs.statSync(p).size, os)}
			w.on('open', () => {console.log('[ Open ]', p);t = setInterval(function(){o()}, itv);})
			w.on('ready', () => {/*console.log('Ready');*/})
			w.on('finish', () => {o();res(true);console.log('[ Finish ]', p)})
			w.on('close', () => {clearInterval(t);console.log('[ close ]', p)})
			w.on('error', () => {res(false);clearInterval(t);console.log('[ Error ]', p)})
		})
    })
}, {1: function (r, h, d) {
		let e = this
		e.ptc = 'https'
		e.sn = 'www'
		e.dmn = 'bilibili.com'
		e.hn = [e.sn, e.dmn].join('.')
		e.r = r
		e.h = h
		e.l = d
		e.clog = console.log
		e.c = (s, c) => {
			let i = s.search(c)
			return i == -1 ? s : s.substring(i+c.length)
		}
		e.mn = u => {
			let c = e.c(u, e.hn)
			switch (c.substring(0, c.lastIndexOf('/'))) {
				case '/video': return 0
				case '/bangumi/play': return 1
				default: return -1
			}
		}
		e.ma = n => {
			switch (n) {
				case 0: return 'https://api.bilibili.com/x/player/playurl'
				case 1: return 'https://api.bilibili.com/pgc/player/web/playurl'
				default: return -1
			}
		}
	}, 2: function (u, g=true) {
		let e = this, m = e.mn(u),
			c = c => {
				let re = /window.__INITIAL_STATE__=.*?;\(function/,
					pl = "window.__INITIAL_STATE__=".length,
					nl = ";(function".length,
					rl = 0,
					r = re.exec(c)
				if (r == null) return null
				rl = r[0].length
				return JSON.parse(r[0].substring(pl, rl-nl))
			}, p = d => {
				let i, p, t, s
				switch (m) {
					case 0: i = d['videoData']; break
					case 1: i = d['mediaInfo']; break
				}
				t = i['title']
				s = {'url': u, 'title': t, 'epInfo': []}
				switch (m) {
					case 0: p = i['pages']; break
					case 1: p = d['epList']; break
				}
				for (let j in p) {
					switch (m) {
						case 0:
							s['epInfo'].push({'cid': p[j]['cid'], 'bvid': i['bvid'], 
											  'id': false, 'ep': j + 1, 'title': p[j]['part']}); break
						case 1:
							s['epInfo'].push({'cid': p[j]['cid'], 'bvid': p[j]['bvid'], 
											  'id': p[j]['id'], 'ep': parseInt(j) + 1, 'title': p[j]['longTitle']}); break
					}
				}
				return s
			}, d = new Promise((res, err) => {
				e.r({"url": u, "headers": e.h()}, r => {
					res(g?p(c(r.data)):c(r.data))
				})
			})
		e.d = () => d
		return d
	}, 3: function (u, e, f, b, c, i=false, sa='') {
		let t = this,
			h = t.h({"cookie": `SESSDATA=${sa};`, "referer": u}),
			p = {"otype": "json", "qn": 16, "fnval": f, "cid": c, "bvid": b},
			d = j => {
				let r = j['result']
				if (r == undefined) r = j['data']
				if (r['durl'] != undefined) return [r['durl'][0]['url']]
				else if (r['dash'] != undefined) {
					let d = r['dash'], 
                        v = d['video'],
                        a = d['audio'][0]
                    for (let i of v)
                        if (i['id'] == e)
                        {v = i; break}
                    return [v['base_url'], a['base_url']]
				} else return false
			},
			hc = (q, r) => {
				if (r.findIndex(r=>r==q) == -1) 
					q > r[0] ? p['qn'] = r[0] : q < r[r.length-1] ? p['qn'] = r[r.length-1] : p['qn'] = q
				e = p['qn']
			}
	    if (i) p['ep_id'] = i
		return new Promise((res, err) => {
			t.r({'url': t.ma(t.mn(u)), 'headers': h, 'params': p}, r => {
				status = r.data['code'] == 0
				if (status) {
					let aq = r.data.data.accept_quality,
					af = r.data.data.accept_format
					hc(e, aq)
					t.r({'url': t.ma(t.mn(u)), 'headers': h, 'params': p}, r => {
						status = r.data['code'] == 0
						if (status) res(d(r.data))
						else err([u, h, p])
					})
				} else console.log(t.ma(t.mn(u)), '[ 请求失败 ]')
			})
		})
	}, 4: function (u, r, p, l=(e, o)=>{console.log(`${parseInt(e/o*100)}%`)}) {
		let e = this
			h = e.h({"accept": "*/*",
					"accept-encoding": "identity",
					"accept-language": "zh-CN,zh;q=0.9",
					"origin": "https://www.bilibili.com",
					"range": "bytes=0-", 
					"referer": r})
		e.clog(p, '>>>>>>>>', 'Loading!')
		return new Promise((res, err) => {
			e.l(u, h, p, e.r, l).then((r, e) => {
				res(r)
			})
		})
	}, 5: function (p, v, a, f) {
		return new Promise((r, e) => {
			process.chdir(p)
			cmd.run(`ffmpeg -i ${v} -i ${a} -codec copy ${f}`,
			function(err, data, stderr){
				let s = err ? false : true
				r(s); e(err)
				console.log(data)
			})
		})
	}, 6: function (u, p='./', ep=64, fnv= 80, f=(n, t)=>`${n}.${t}`) {
		let t = this
		t[2](u).then(r => { 
			let d = r['epInfo'],
				dn = r['title'],
				e = fs.existsSync(p),
				/* 
					这里比较建议使用 string 的 replace(正则公式,替换字符)
					s = s.replace(/[^[\\\/:*?<>|\s]]/g, '_')
				*/
				pfn = (s, sig='_') => {
					let a = ['\\', '/', ':', '*', '?', '<', '>', '|', ' ']
					s = s.split('')
					for (let i in s) 
						if (a.findIndex(r=>r==s[i]) == 1) s[i] = sig
					return s.join('')
				}
			if (e) {let s = fs.statSync(p); if (!s.isDirectory) fs.mkdirSync(p)}
			else {fs.mkdirSync(p)}
			p = path.join(p, dn)
			if (!fs.existsSync(p)) fs.mkdirSync(p)
			d.map(i => {
				let fn = f(i['ep'], d.length == 1 && i['title'] == '' ? dn : i['title']),
				lfn = `${pfn(fn).split(' ').join('_')}.flv`,
				plfn = path.join(p, lfn)
				t[3](u, ep, fnv, i['bvid'], i['cid'], i['id']).then(r => {
					let l = r.length
					switch (l) {
						case 1:
							t.FileDownload(r[0], u, path.join(p, lfn)); break
						case 2:
							let v = `v_${i['ep']}.m4s`, 
							a = `a_${i['ep']}.m4s`,
							vp = path.join(p, v),
							ap = path.join(p, a),
							EventEmitter = new events.EventEmitter(),
							delp = () => {
								fs.unlink(vp, err => {if (err) console.log(err)})
								fs.unlink(ap, err => {if (err) console.log(err)})
							}
							async function d () {
								let vs = await t[4](r[0], u, vp),
								va = await t[4](r[1], u, ap)
								return vs && va
							}
							d().then(r => {
								let s = fs.existsSync(plfn)
								if (r && !s) t[5](p, v, a, lfn).then(l => {
									EventEmitter.emit('read', l);
								})
							})
							EventEmitter.on('read', function(res){
							    //处理异步读取得到的数据
							    if (res) delp()
								else if (!fs.existsSync(plfn)) delp()
							})
							break
					}
				}).catch(e => {console.log(e)})
			})
		})
	}
})

bilibili-interface.js

const { bili } = require('./bilibili.js');

class BiliBili {
    constructor(){this.sessdata='' }
    setSessdata(ps){this.sessdata=ps}
    MultiEpisodesInfo(url, parse, callback){bili[2](url, parse).then(r=>{callback(r)})}
    MultiEpisodesDownload(url, path, ep, fnval, format){bili[6](url, path, ep, fnval, format)}
    VideoAudioJoin(path, video, audio, file){bili[5](path, video, audio, file)}
    VideoDownload(url, referer, path, format){bili[4](url, referer, path, format)}
    VideoInfo=(url, ep, fnval, bvid, cid, epid=false)=>bili[3](url, ep, fnval, bvid, cid, epid, this.sessdata)
}

module.exports = { BiliBili }

download.js

const { BiliBili } = require('./bilibili-interface.js')
/*
	功能类的实例化(主要用于下载,局部页面数据仅供使用)
*/
var b = new BiliBili()
/* 
	Sessdata 身份验证:存放于 Cookie, 每登录一次,该id会自动变更 
*/
b.setSessdata('')
/*
	页面基本数据获取,可获取解析前后的数据,parse(true/false)
*/
b.MultiEpisodesInfo('https://www.bilibili.com/bangumi/play/ss33378?t=270', true, r => {
    let d = r['epInfo']
    /*
		获取具体集数(episodes)的视频播放数据,可用于下载
	*/
    b.VideoInfo('https://www.bilibili.com/bangumi/play/ss33378?t=270', 64, 80,  d[2]['bvid'], d[2]['cid'], d[2]['id']).then(o => {
        console.log(o)
    })
})
/*
	视频多p下载, 后面的 n 为p号,t 为各p标题,具体作用是什么格式输出文件名
*/
b.MultiEpisodesDownload('https://www.bilibili.com/video/BV1us411z7uV', './', 64, 80, (n, t) => t)

bilibili视频批量合并工具.zip是一个用于合并bilibili视频的工具压缩文件。该工具可以帮助用户将多个bilibili视频文件合并为一个完整的视频文件。使用该工具,用户可以方便地将分段的视频合并为长视频,更好地观看和分享。 这个工具压缩文件通常包含了一个可执行程序或脚本以及相应的依赖文件,用户需要将其解压并运行其中的可执行程序或脚本来使用。使用该工具的过程一般包括以下几个步骤: 1. 解压工具压缩文件:将下载bilibili视频批量合并工具.zip文件解压到指定的文件夹中。 2. 运行工具程序或脚本:根据工具的说明文档,找到可执行程序或脚本,并运行它。可能需要根据具体情况进行配置和设置。 3. 导入要合并的视频文件:根据工具提供的界面或命令行参数,选择要合并的视频文件,并导入到工具中。 4. 设置合并参数:根据需要设置合并的参数,例如输出文件的格式、名称、分辨率等。 5. 合并视频文件:点击开始合并或运行相应的命令,工具将自动将多个视频文件合并为一个完整的视频文件。合并的过程可能需要一定的时间,取决于视频文件的数量和大小。 6. 完成合并:等待工具运行完成并提示合并成功后,可以在指定的输出文件夹中找到合并后的视频文件。 总之,bilibili视频批量合并工具.zip是一个帮助用户合并bilibili视频的实用工具。用户只需下载并解压该压缩文件,按照工具的使用说明进行操作,即可将多个bilibili视频合并为一个完整的视频文件,提升观看和分享的便利性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值