feat: first commit

This commit is contained in:
lyz05
2022-11-08 10:26:40 +08:00
parent f68e4a6743
commit 42887a780a
32 changed files with 11077 additions and 0 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
Dockerfile
.dockerignore
node_modules
.git

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

12
.idea/danmaku.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/danmaku.iml" filepath="$PROJECT_DIR$/.idea/danmaku.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
FROM debian:bullseye as builder
ARG NODE_VERSION=16.18.0
RUN apt-get update; apt install -y curl
RUN curl https://get.volta.sh | bash
ENV VOLTA_HOME /root/.volta
ENV PATH /root/.volta/bin:$PATH
RUN volta install node@${NODE_VERSION}
#######################################################################
RUN mkdir /app
WORKDIR /app
ENV NODE_ENV production
COPY . .
RUN npm install
FROM debian:bullseye
LABEL fly_launch_runtime="nodejs"
COPY --from=builder /root/.volta /root/.volta
COPY --from=builder /app /app
WORKDIR /app
ENV NODE_ENV production
ENV PATH /root/.volta/bin:$PATH
CMD [ "npm", "run", "start" ]

23
README.md Normal file
View File

@@ -0,0 +1,23 @@
# 阿里云函数计算 Django项目
包含如下子项目
- airportsub: 用于机场订阅
- danmaku用于弹幕解析
- ipinfo: 用纯真IP数据库查询IP信息
# 依赖
- chai: 断言库
- mocha: 测试框架
- ejs: 模板引擎
- express: web框架
- lib-qqwry: 纯真IP数据库
# 部署到fly.io
```
curl -L https://fly.io/install.sh | sh
flyctl auth login
flyctl deploy
```
# 性能提升
相比于旧版的Python项目Node对于异步并发的处理能力更强。
Express框架的性能也比Python的Django要好很多。

49
app.js Normal file
View File

@@ -0,0 +1,49 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// 引入环境变量
require('dotenv').config();
// 引入一个个路由模块
var danmakuRouter = require('./routes/danmaku');
var usersRouter = require('./routes/users');
var ipinfoRouter = require('./routes/ipinfo');
var airportsubRouter = require('./routes/airportsub');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', danmakuRouter);
app.use('/users', usersRouter);
app.use('/ipinfo', ipinfoRouter);
app.use('/sub', airportsubRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

90
bin/www Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('danmaku:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

39
fly.toml Normal file
View File

@@ -0,0 +1,39 @@
# fly.toml file generated for shy-field-2671 on 2022-11-07T23:03:33+08:00
app = "shy-field-2671"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[env]
PORT = "8080"
[experimental]
allowed_public_ports = []
auto_rollback = true
[[services]]
http_checks = []
internal_port = 8080
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"

9551
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "danmaku",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon ./bin/www",
"test": "nyc -a mocha --recursive"
},
"dependencies": {
"ali-oss": "^6.17.1",
"axios": "^1.1.3",
"cheerio": "^1.0.0-rc.12",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"dotenv": "^16.0.3",
"ejs": "^3.1.8",
"express": "~4.16.1",
"filesize": "^10.0.5",
"got": "^11.8.2",
"http-errors": "~1.6.3",
"js-yaml": "^4.1.0",
"lib-qqwry": "^1.3.2",
"moment": "^2.29.4",
"morgan": "~1.9.1",
"pako": "^1.0.11",
"xml-js": "^1.6.11"
},
"devDependencies": {
"chai": "^4.3.6",
"chai-http": "^4.3.0",
"mocha": "^10.1.0",
"nodemon": "^2.0.20",
"nyc": "^15.1.0"
}
}

View File

@@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

150
routes/airportsub.js Normal file
View File

@@ -0,0 +1,150 @@
const express = require('express');
const router = express.Router();
const oss = require('../utils/oss');
const yaml = require('js-yaml');
const cookie = require('cookie');
const {filesize} = require('filesize');
const moment = require('moment');
const axios = require('axios');
function getscheme(req) {
return req.headers['x-forwarded-proto'] || req.protocol;
}
function getuserinfo(headers) {
const str = headers["subscription-userinfo"]
if (str === undefined) {
return undefined
}
var dic = cookie.parse(str);
dic['total_use'] = 1 * dic.upload + 1 * dic.download
dic['use_percent'] = (100.0 * dic.total_use / dic.total).toFixed(2)
const now_time = Math.floor(Date.now() / 1000)
const end_time = dic.expire
dic['date_percent'] = (100.0 * (now_time - end_time + 3600 * 24 * 365.0) / (3600 * 24 * 365.0)).toFixed(2)
dic['total_use'] = filesize(dic['total_use'], {base: 2, standard: "jedec"})
dic.total = filesize(dic.total, {base: 2, standard: "jedec"})
dic.expire = moment(dic.expire * 1000).format('YYYY-MM-DD')
return dic
}
async function updateDatabase() {
const database = await oss.get('SUB/database.yaml');
try {
const doc = yaml.load(database);
return doc
} catch (e) {
console.log(e);
}
}
/* GET users listing. */
router.get('/', async function (req, res, next) {
const database = await updateDatabase();
if (req.query.user) {
const userinfo = database.user[req.query.user]
if (userinfo) {
const expireDate = new Date(userinfo.expire);
const now = new Date();
if (now < expireDate) {
if (req.query.ctype) {
const subinfo = database.suburl[req.query.ctype]
//返回指定订阅信息
if (subinfo) {
const ret = await oss.get('SUB/' + req.query.ctype)
res.type('text/plain').end(ret);
} else {
res.status(404).send('Not Found 找不到这种订阅类型');
}
} else {
const path = getscheme(req) + '://' + req.headers.host + req.originalUrl;
const ctypes = Object.keys(database.suburl)
let ret = {}
for (key of ctypes) {
const headers = await oss.head('SUB/' + key)
ret[key] = getuserinfo(headers)
// ret[key] = getuserinfotxt(getuserinfo(headers))
}
res.render('airportsub', {ret, path, expire: userinfo.expire});
}
} else {
res.send('您的订阅已过期,请联系管理员');
}
} else {
res.status(404).send('Not Found 找不到这个用户');
}
} else {
res.status(400).send('Bad Request 缺少参数');
}
});
router.get('/cache', async function (req, res, next) {
const database = await updateDatabase();
let messages = [];
// 缓存所有的协程
let promises = [];
for (let key in database.suburl) {
const url = database.suburl[key].url
const params = database.suburl[key].params
if (!url) continue
promises.push(axios.get(url, {params}));
}
Promise.all(promises).then(values => {
promises = [];
for (let i = 0; i < values.length; i++) {
const res = values[i];
const key = Object.keys(database.suburl)[i]
messages.push({title: 'Download', key, status: res.status})
const userinfo = res.headers['subscription-userinfo']
const base64userinfo = btoa(userinfo)
// 设置强制下载并设置文件名
headers = {
'Content-type': 'text/plain; charset=utf-8',
'content-disposition': `attachment; filename=${key}`,
'x-oss-persistent-headers': "Subscription-Userinfo:" + base64userinfo
}
promises.push(oss.put('SUB/' + key, res.data, headers))
}
Promise.all(promises).then(values => {
for (let i = 0; i < values.length; i++) {
const res = values[i];
const key = Object.keys(database.suburl)[i]
messages.push({title: 'Upload', key, status: res.status})
}
res.json(messages);
})
});
});
router.get('/download', async function (req, res, next) {
const repos = ['Dreamacro/clash', 'Fndroid/clash_for_windows_pkg', 'Kr328/ClashForAndroid',
'shadowsocks/shadowsocks-android', 'XTLS/Xray-core', '2dust/v2rayN', 'NetchX/Netch', '2dust/v2rayNG',
'yichengchen/clashX', 'shadowsocks/shadowsocks-windows',
'shadowsocksrr/shadowsocksr-csharp', 'FelisCatus/SwitchyOmega']
const auth = {
'username': process.env.GITHUB_USERNAME,
'password': process.env.GITHUB_TOKEN
}
const api = 'https://api.github.com/repos/{}/releases/latest'
const promises = repos.map(repo => axios.get(api.replace('{}', repo), {auth}))
Promise.all(promises).then(values => {
let datas = values.map(value => value.data)
for (let i = 0; i < datas.length; i++) {
datas[i].repo = repos[i]
for (asset of datas[i].assets) {
asset.size = filesize(asset.size, {base: 2, standard: "jedec"})
asset['fastgit_url'] = asset['browser_download_url'].replace('github.com', 'download.fastgit.org')
asset['ghproxy_url'] = 'https://mirror.ghproxy.com?q=' + asset['browser_download_url']
}
}
res.render('airportdownload', {datas});
});
});
module.exports = router;
if (!module.parent) {
updateDatabase();
}

14
routes/api/base.js Normal file
View File

@@ -0,0 +1,14 @@
//引入API组件
const Bilibili = require('./bilibili');
const Mgtv = require('./mgtv');
const Tencentvideo = require('./tencentvideo');
const Youku = require('./youku');
const Iqiyi = require('./iqiyi');
// 实例化API组件
const bilibili = new Bilibili();
const mgtv = new Mgtv();
const tencentvideo = new Tencentvideo();
const youku = new Youku();
const iqiyi = new Iqiyi();
module.exports = { bilibili, mgtv, tencentvideo, youku, iqiyi };

97
routes/api/bilibili.js Normal file
View File

@@ -0,0 +1,97 @@
const urlmodule = require('url');
const axios = require('axios');
const got = require('got');
const {inflateRawSync} = require('zlib');
function Bilibili() {
this.name = 'B站';
this.domain = 'bilibili.com';
this.example_urls = [
'https://www.bilibili.com/video/av170001',
'https://www.bilibili.com/video/av170001?p=2',
'https://www.bilibili.com/video/BV17x411w7KC?p=3',
'https://www.bilibili.com/bangumi/play/ep691614'
];
this.resolve = async (url) => {
// 相关API
const api_video_info = "https://api.bilibili.com/x/web-interface/view"
const api_epid_cid = "https://api.bilibili.com/pgc/view/web/season"
var q = urlmodule.parse(url, true);
var path = q.pathname.split('/');
// 普通投稿视频
if (url.indexOf('video/') !== -1) {
// 获取视频分P信息
const p = q.query.p || 1;
// 判断是否为旧版av号
var params = {};
if (url.indexOf('BV') !== -1) {
params = {'bvid': path.slice(-1)[0]};
} else {
params = {'aid': path.slice(-1)[0].substring(2)};
}
response = await axios.get(api_video_info, {params})
if (response.data.code !== 0) {
this.error_msg = '获取普通投稿视频信息失败!'
return
}
this.title = response.data.data.title;
const subtitle = response.data.data.pages[p - 1].part;
this.title = this.title + '-' + subtitle;
const cid = response.data.data.pages[p - 1].cid;
return [`https://comment.bilibili.com/${cid}.xml`];
} // 番剧
else if (url.indexOf('bangumi/') !== -1) {
const epid = path.slice(-1)[0];
const params = {'ep_id': epid.slice(2)};
response = await axios.get(api_epid_cid, {params})
if (response.data.code !== 0) {
this.error_msg = '获取番剧视频信息失败!'
return
}
for (var i = 0; i < response.data.result.episodes.length; i++) {
if (response.data.result.episodes[i].id == params.ep_id) {
this.title = response.data.result.episodes[i].share_copy;
const cid = response.data.result.episodes[i].cid;
return [`https://comment.bilibili.com/${cid}.xml`];
}
}
} else {
this.error_msg = '不支持的B站视频网址仅支持普通视频(av,bv)、剧集视频(ep)';
}
}
this.parse = async (urls) => {
// B站使用特殊的压缩方法需要使用got模块
const bufferData = await got(urls[0], {
decompress: false
}).buffer();
const content = inflateRawSync(bufferData).toString();
return content
}
this.work = async (url) => {
urls = await this.resolve(url);
console.log(this.name,'api lens:',urls.length);
if (!this.error_msg)
this.content = await this.parse(urls);
return {
title: this.title,
content: this.content,
msg: this.error_msg? this.error_msg: 'ok'
}
}
}
module.exports = Bilibili
if(!module.parent) {
const b = new Bilibili();
b.work(b.example_urls[0]).then(() => {
console.log(b.content);
console.log(b.title);
});
}

95
routes/api/iqiyi.js Normal file
View File

@@ -0,0 +1,95 @@
const axios = require('axios');
const convert = require('xml-js');
const pako = require('pako');
const cheerio = require('cheerio');
const {time_to_second, make_response, content_template} = require('./utils');
function Iqiyi() {
this.name = '爱奇艺'
this.domain = 'iqiyi.com'
this.example_urls = [
'https://www.iqiyi.com/v_19rr1lm35o.html'
];
this.resolve = async (url) => {
const res = await axios({
method: 'get',
url: "https://proxy-fc-python-fdssfsqzaa.cn-shenzhen.fcapp.run/",
params: {url},
auth: {username: 'proxy', password: 'proxy'}
});
const data = res.data
const result = data.match(/window.Q.PageInfo.playPageInfo=(.*);/)
const page_info = JSON.parse(result[1])
// console.log('page_info:', page_info)
const duration = time_to_second(page_info.duration)
this.title = page_info.tvName ? page_info.tvName : page_info.name
const albumid = page_info.albumId
const tvid = page_info.tvId.toString()
const categoryid = page_info.cid
page = Math.floor(duration / (60 * 5)) + 1
console.log('tvid', tvid)
let promises = []
for (let i = 0; i < page; i++) {
const api_url = `http://cmts.iqiyi.com/bullet/${tvid.slice(-4, -2)}/${tvid.slice(-2)}/${tvid}_300_${i + 1}.z`
const params = {
rn: '0.0123456789123456',
business: 'danmu',
is_iqiyi: 'true',
is_video_page: 'true',
tvid: tvid,
albumid: albumid,
categoryid: categoryid,
qypid: '01010021010000000000'
}
promises.push(axios({method: 'get', url: api_url, params: params, responseType: 'arraybuffer'}))
}
return promises
}
this.parse = async (promises) => {
let contents = [];
const values = await Promise.all(promises)
let datas = values.map(value => value.data)
for (const data of datas) {
const xml = pako.inflate(data, {to: 'string'})
const $ = cheerio.load(xml, {xmlMode: true});
$('bulletInfo').each(function (i, elem) {
var content = JSON.parse(JSON.stringify(content_template));
content.timepoint = $(this).find('showTime').text()//showTime
content.color = parseInt($(this).find('color').text(), 16)//color
content.content = $(this).find('content').text() //content
content.size = $(this).find('font').text()//font
contents.push(content);
})
}
contents = make_response(contents)
return contents
}
this.work = async (url) => {
const promises = await this.resolve(url);
console.log(this.name, 'api lens:', promises.length)
this.content = await this.parse(promises);
return {
title: this.title,
content: this.content,
msg: 'ok'
}
}
}
module.exports = Iqiyi
if (!module.parent) {
const m = new Iqiyi();
m.work(m.example_urls[0]).then(() => {
console.log(m.content);
console.log(m.title);
});
}

75
routes/api/mgtv.js Normal file
View File

@@ -0,0 +1,75 @@
const urlmodule = require('url');
const axios = require('axios');
const convert = require('xml-js');
const {time_to_second, make_response, content_template} = require('./utils');
function Mgtv() {
this.name = '芒果TV'
this.domain = 'mgtv.com'
this.example_urls = [
'https://www.mgtv.com/b/336727/8087768.html'
];
this.resolve = async (url) => {
const api_video_info = "https://pcweb.api.mgtv.com/video/info"
const api_danmaku = 'https://galaxy.bz.mgtv.com/rdbarrage'
const q = urlmodule.parse(url, true);
const path = q.pathname.split('/');
const cid = path.slice(-2)[0];
const vid = path.slice(-1)[0].split('.')[0];
const res = await axios.get(api_video_info, {params: {cid, vid}});
this.title = res.data.data.info.videoName;
const time = res.data.data.info.time;
const step = 60 * 1000;
const end_time = time_to_second(time) * 1000;
let promises = [];
for (let i = 0; i < end_time; i += step) {
promises.push(axios({method: 'get', url: api_danmaku, params: {vid, cid, time: i}}));
}
return promises
}
this.parse = async (promises) => {
let contents = [];
const values = await Promise.all(promises)
let datas = values.map(value => value.data)
for (const data of datas) {
if (data.data.items === null)
continue;
for (const item of data.data.items) {
var content = JSON.parse(JSON.stringify(content_template));
content.timepoint = item.time / 1000;
content.content = item.content;
content.uid = item.uid;
contents.push(content);
}
}
contents = make_response(contents)
return contents
}
this.work = async (url) => {
const promises = await this.resolve(url);
console.log(this.name,'api lens:', promises.length)
this.content = await this.parse(promises);
return {
title: this.title,
content: this.content,
msg: 'ok'
}
}
}
module.exports = Mgtv
if (!module.parent) {
const m = new Mgtv();
m.work(m.example_urls[0]).then(() => {
console.log(m.content);
console.log(m.title);
});
}

View File

@@ -0,0 +1,83 @@
const urlmodule = require('url');
const axios = require('axios');
const convert = require('xml-js');
const cheerio = require("cheerio");
const {make_response, content_template} = require('./utils');
function Tencentvideo() {
this.name = '腾讯视频'
this.domain = 'v.qq.com'
this.example_urls = [
'https://v.qq.com/x/cover/mzc002003pn34qk/u3319i5s3jt.html'
];
this.resolve = async (url) => {
const api_danmaku_base = "https://dm.video.qq.com/barrage/base/"
const api_danmaku_segment = "https://dm.video.qq.com/barrage/segment/"
const q = urlmodule.parse(url, true);
const path = q.pathname.split('/');
let vid;
if (q.query.vid) {
vid = q.query.vid
} else {
vid = path.slice(-1)[0].split('.')[0];
}
console.log('vid:', vid)
let res = await axios.get(url);
const $ = cheerio.load(res.data, null, false);
this.title = $("title")[0].children[0].data;
res = await axios.get(api_danmaku_base + vid);
let promises = []
let list = Object.values(res.data.segment_index)
for (item of list) {
promises.push(axios.get(`${api_danmaku_segment}${vid}/${item.segment_name}`))
}
return promises
}
this.parse = async (promises) => {
let contents = [];
const values = await Promise.all(promises)
let datas = values.map(value => value.data)
for (const data of datas) {
for (const item of data.barrage_list) {
var content = JSON.parse(JSON.stringify(content_template));
content.timepoint = item.time_offset / 1000;
if (item.content_style.color) {
const content_style = JSON.stringify(item.content_style.color)
console.log("有颜色", content_style);
}
content.content = item.content;
contents.push(content);
}
}
contents = make_response(contents)
return contents
}
this.work = async (url) => {
promises = await this.resolve(url);
console.log(this.name, 'api lens:', promises.length)
this.content = await this.parse(promises);
return {
title: this.title,
content: this.content,
msg: 'ok'
}
}
}
module.exports = Tencentvideo
if (!module.parent) {
console.log('main')
const t = new Tencentvideo()
t.work(t.example_urls[0]).then(() => {
console.log(t.content)
console.log(t.title)
});
}

50
routes/api/utils.js Normal file
View File

@@ -0,0 +1,50 @@
const convert = require("xml-js");
const content_template = {
timepoint: 0,
content: '',
ct: 1,
size: 20,
color: 16777215,
unixtime: Math.floor(Date.now() / 1000),
uid: 0,
};
function time_to_second(time) {
var t = time.split(':');
var s = 0;
var m = 1;
while (t.length > 0) {
s += m * parseInt(t.pop(), 10);
m *= 60;
}
return s;
}
function make_response(contents) {
let xml = {
_declaration: {
_attributes: {
version: '1.0',
encoding: 'utf-8'
}
},
i: {
d: []
}
}
for (let content of contents) {
xml.i.d.push({
_attributes: {
p: `${content.timepoint},${content.ct},${content.size},${content.color},${content.unixtime},${content.uid},26732601000067074`
},
_text: content.content
});
}
const res = convert.js2xml(xml, {compact: true, spaces: 4})
return res
}
module.exports = {time_to_second, make_response, content_template};

149
routes/api/youku.js Normal file
View File

@@ -0,0 +1,149 @@
const urlmodule = require('url');
const axios = require('axios');
const convert = require('xml-js');
const cookie = require('cookie');
const crypto = require('crypto');
const {make_response, content_template} = require('./utils');
function Youku() {
this.name = '优酷'
this.domain = 'v.youku.com'
this.example_urls = [
'https://v.youku.com/v_show/id_XNTE5NjUxNjUyOA==.html'
];
this.get_tk_enc = async () => {
api_url = "https://acs.youku.com/h5/mtop.com.youku.aplatform.weakget/1.0/?jsv=2.5.1&appKey=24679788"
const res = await axios.get(api_url);
const cookies = res.headers['set-cookie']
let targetCookie = {};
for (let cookieStr of cookies) {
targetCookie = Object.assign(targetCookie, cookie.parse(cookieStr));
}
return targetCookie
}
this.get_cna = async () => {
api_url = "https://log.mmstat.com/eg.js"
const res = await axios.get(api_url);
const cookies = res.headers['set-cookie']
let targetCookie = {};
for (let cookieStr of cookies) {
targetCookie = Object.assign(targetCookie, cookie.parse(cookieStr));
}
return targetCookie['cna']
}
const yk_msg_sign = (msg) => {
var md5 = crypto.createHash('md5');
return md5.update(msg + "MkmC9SoIw6xCkSKHhJ7b5D2r51kBiREr").digest('hex');
}
const yk_t_sign = (token, t, appkey, data) => {
text = [token, t, appkey, data].join('&');
var md5 = crypto.createHash('md5');
return md5.update(text).digest('hex')
}
const get_vinfos_by_video_id = async (url) => {
const q = urlmodule.parse(url, true);
const path = q.pathname.split('/');
const video_id = path.slice(-1)[0].split('.')[0].slice(3);
const duration = 0
if (video_id) {
// "?client_id=53e6cc67237fc59a&package=com.huawei.hwvplayer.youku&ext=show&video_id={}"
api_url = "https://openapi.youku.com/v2/videos/show.json"
params = {
client_id: "53e6cc67237fc59a",
video_id: video_id,
package: "com.huawei.hwvplayer.youku",
ext: "show"
}
const res = await axios.get(api_url, {params: params})
const duration = res.data.duration
this.title = res.data.title
console.log("video_id:", video_id, 'duration:', duration, 'title:', this.title)
return [video_id, duration]
}
}
this.work = async (url) => {
const cna = await this.get_cna()
const tk_enc = await this.get_tk_enc()
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Cookie": '_m_h5_tk=' + tk_enc['_m_h5_tk'] + ';_m_h5_tk_enc=' + tk_enc['_m_h5_tk_enc'] + ';',
"Referer": "https://v.youku.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
}
const [vid, duration] = await get_vinfos_by_video_id(url)
let contents = [];
const max_mat = Math.floor(duration / 60) + 1
console.log(this.name, 'api lens:', max_mat)
for (let mat = 0; mat < max_mat; mat++) {
api_url = "https://acs.youku.com/h5/mopen.youku.danmu.list/1.0/"
msg = {
"ctime": Date.now(),
"ctype": 10004,
"cver": "v1.0",
"guid": cna,
"mat": mat,
"mcount": 1,
"pid": 0,
"sver": "3.1.0",
"type": 1,
"vid": vid
}
// plain-text string
const str = JSON.stringify(msg);
const buff = Buffer.from(str, 'utf-8');
const msg_b64encode = buff.toString('base64');
msg['msg'] = msg_b64encode
msg['sign'] = yk_msg_sign(msg_b64encode)
data = JSON.stringify(msg)
t = Date.now()
params = {
"jsv": "2.5.6",
"appKey": "24679788",
"t": t,
"sign": yk_t_sign(tk_enc["_m_h5_tk"].slice(0, 32), t, "24679788", data),
"api": "mopen.youku.danmu.list",
"v": "1.0",
"type": "originaljson",
"dataType": "jsonp",
"timeout": "20000",
"jsonpIncPrefix": "utility"
}
const res = await axios.post(api_url, {data}, {headers: headers, params: params})
danmus = JSON.parse(res.data.data.result).data.result
// 接口请求情况
console.log(mat, res.data.ret[0])
for (danmu of danmus) {
var content = JSON.parse(JSON.stringify(content_template));
content.timepoint = danmu["playat"] / 1000
if (danmu.propertis.color) {
content.color = JSON.parse(danmu.propertis).color
}
content.content = danmu.content
contents.push(content)
}
}
contents = make_response(contents)
this.content = contents
return {
title: this.title,
content: this.content,
msg: 'ok'
}
}
}
module.exports = Youku
if (!module.parent) {
const b = new Youku();
b.work(b.example_urls[0]).then(() => {
console.log(b.content);
console.log(b.title);
});
}

53
routes/danmaku.js Normal file
View File

@@ -0,0 +1,53 @@
const express = require('express');
const axios = require('axios');
const router = express.Router();
const { bilibili, mgtv, tencentvideo, youku, iqiyi } = require('../routes/api/base');
const list = [bilibili, mgtv, tencentvideo, youku, iqiyi];
function getscheme(req) {
return req.headers['x-forwarded-proto'] || req.protocol;
}
async function build_response(url, download) {
try {
const res = await axios.get(url)
} catch (error) {
console.log(error)
return {'msg': '传入的链接非法!请检查链接是否能在浏览器正常打开'}
}
var fc = undefined
for (var item of list) {
if (url.indexOf(item.domain) !== -1) {
fc = item
}
}
if (fc === undefined) {
return {'msg': '不支持的视频网址'}
}
return await fc.work(url)
}
/* GET home page. */
router.get('/', async function (req, res, next) {
//检查是否包含URL参数
if (!req.query.url) {
var urls = [mgtv.example_urls[0], bilibili.example_urls[0], tencentvideo.example_urls[0], youku.example_urls[0], iqiyi.example_urls[0]];
const path = getscheme(req) + '://' + req.headers.host + req.originalUrl;
res.render('danmaku', {path, urls});
} else {
url = req.query.url;
download = (req.query.download === 'on');
ret = await build_response(url, download)
if (ret.msg !== 'ok') {
res.status(403).send(ret.msg)
} else if (download) {
res.attachment(ret.title + '.xml');
res.end(ret.content);
} else {
res.type('application/xml');
res.end(ret.content);
}
}
});
module.exports = router;

32
routes/ipinfo.js Normal file
View File

@@ -0,0 +1,32 @@
var express = require('express');
var router = express.Router();
var libqqwry = require('lib-qqwry');
var dns = require('dns');
var qqwry = libqqwry() //初始化IP库解析器
function getClientIp(req) {
return req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress;
}
/* GET home page. */
router.get('/', function (req, res, next) {
var ip = req.query.name || getClientIp(req);
dns.lookup(ip, (err, address, family) => {
if (err) {
ipL = { 'ip': ip, 'msg': '域名解析IP失败' };
} else {
ip = address
try {
var ipL = qqwry.searchIP(ip); //查询IP信息
} catch (e) {
ipL = { 'ip': ip, 'msg': e };
}
}
res.json(ipL);
});
});
module.exports = router;

9
routes/users.js Normal file
View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});
module.exports = router;

26
test.js Normal file
View File

@@ -0,0 +1,26 @@
function main() {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1');
}, 500);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2');
}, 300);
});
const Promises = [p1,p2];
let count = 0;
p1.then((value) => {
count ++;
console.log(value)
})
p2.then((value) => {
count ++;
console.log(value)
})
while (count<2) ;
console.log('All promise done');
}
main()

80
test/App.test.js Normal file
View File

@@ -0,0 +1,80 @@
let chai = require('chai');
let chaiHttp = require('chai-http');
let app = require('../app');
const { bilibili, mgtv, tencentvideo, youku, iqiyi } = require('../routes/api/base');
const list = [bilibili, mgtv, tencentvideo, youku, iqiyi];
let should = chai.should();
chai.use(chaiHttp);
describe('App', () => {
describe('弹幕解析模块测试', function () {
this.timeout(1000*60);
it('主页测试', (done) => {
chai.request(app)
.get('/')
.end((err, res) => {
res.should.have.status(200);
done();
});
});
for (const item of list) {
const name = item.name;
const example_urls = item.example_urls;
for (const i in example_urls) {
const url = example_urls[i];
it(name+'视频测试#'+i, (done) => {
chai.request(app)
.get('/')
.query({url})
.end((err, res) => {
res.should.have.status(200);
res.header['content-type'].should.equal('application/xml');
done();
});
});
}
}
});
describe('users modules', () => {
it('should GET the users response', (done) => {
chai.request(app)
.get('/users')
.end((err, res) => {
res.should.have.status(200);
res.text.should.equal('respond with a resource');
done();
});
});
});
it('should respond status 404', (done) => {
chai.request(app)
.get('/wrongUrl')
.end((err, res) => {
res.should.have.status(404);
done();
});
});
describe('ipinfo modules', () => {
it('GET the ipinfo response', (done) => {
chai.request(app)
.get('/ipinfo')
.end((err, res) => {
res.should.have.status(200);
done();
});
});
it('GET the ipinfo with name', (done) => {
chai.request(app)
.get('/ipinfo?name=home999.cc')
.end((err, res) => {
res.should.have.status(200);
done();
});
});
});
});

58
utils/oss.js Normal file
View File

@@ -0,0 +1,58 @@
const OSS = require('ali-oss');
const normalendpoint = 'oss-cn-hongkong.aliyuncs.com';
const fastendpoint = 'oss-accelerate.aliyuncs.com';
// 引入环境变量
require('dotenv').config({path: '../.env'});
let client = new OSS({
region: process.env.OSS_REGION,
accessKeyId: process.env.OSS_ACCESS_KEY,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
bucket: process.env.OSS_BUCKET,
});
async function get(objname) {
try {
const result = await client.get(objname);
return result.content.toString()
} catch (e) {
console.log(e);
}
}
async function put(objname, content, headers) {
try {
const result = await client.put(objname, new Buffer.from(content), {headers});
return result
} catch (e) {
console.log(e);
}
}
async function head(objname) {
try {
const result = await client.head(objname);
return result.res.headers
} catch (e) {
console.log(e);
}
}
async function signurl(objname) {
try {
const result = await client.signatureUrl(objname);
return result
} catch (e) {
console.log(e);
}
}
module.exports = {get, put, head, signurl};
if (!module.parent) {
get('SUB/database.yaml');
put('SUB/test.txt', '中文');
head('SUB/database.yaml');
signurl('SUB/database.yaml');
}

74
views/airportdownload.ejs Normal file
View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>代理软件下载链接</title>
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.4.1/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1>代理软件下载加速链接:</h1>
<p>
以下是各个代理软件(含浏览器扩展)的Github最新版本:</p>
<ul>
<% for (item of datas) { %>
<li>
<a href="#<%= item.repo %>"><%= item.repo %></a>
</li>
<% } %>
</ul>
<% for (item of datas) { %>
<h2 id="<%= item.repo %>"><%= item.repo %>
<small><%= item.tag_name %></small>
</h2>
<p>
<table class="table table-striped">
<thead>
<tr>
<th>文件名</th>
<th>文件大小</th>
<th>下载次数</th>
<th>修改时间</th>
<th>链接</th>
</tr>
</thead>
<tbody>
<% for(asset of item.assets){ %>
<tr>
<td>
<a href="<%= asset.fastgit_url %>">
<span><%= asset.name %></span>
</a>
</td>
<td>
<%= asset.size %>
</td>
<td>
<%= asset.download_count %>
</td>
<td>
<%= asset.updated_at %>
</td>
<td>
<div class="btn-group btn-group-sm">
<a class="btn btn-default" href="<%= asset.browser_download_url %>">
<span>原始链接</span>
</a>
<a class="btn btn-default" href="<%= asset.fastgit_url %>">
<span>fastgit</span>
</a>
<a class="btn btn-default" href="<%= asset.ghproxy_url %>">
<span>ghproxy</span>
</a>
</div>
</td>
</tr>
<% } %>
</tbody>
</table>
<% } %>
</div>
</body>
</html>

45
views/airportsub.ejs Normal file
View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>订阅信息</title>
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.4.1/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h3>温馨提示:</h3>
<p>
带filter的是过滤无效节点并精选的结果建议优先使用。强烈建议使用clash作为客户端具有自动测试节点的功能和完善的规则。</p>
<p>
相关软件<a href="/sub/download">下载链接</a><br>
</p>
<p>
<p class="card-heading">Telegram 代理</p>
<p>
<a href="tg://proxy?server=vipserv.ccloud.live&amp;port=443&amp;secret=dddd561961fea026e517764b084bd64072">域名</a>
<a href="tg://proxy?server=167.235.77.32&amp;port=443&amp;secret=dddd561961fea026e517764b084bd64072">IPV4</a>
<a href="tg://proxy?server=2a01:4f8:1c1e:eeb6::1&amp;port=443&amp;secret=dddd561961fea026e517764b084bd64072">IPV6</a>
</p>
</p>
<h3>Your Subscribe:</h3>
<p>
当前账户过期时间:<%= expire %><br>
<% for (const index in ret) { %>
<%= index %>:
<a href="<%= path %>&ctype=<%= index %>">
<%= path %>&ctype=<%= index %>
</a>
<br>
<% if (ret[index]) { %>
过去已用:<%= ret[index].total_use %> 总量:<%= ret[index].total %> 过期时间:<%= ret[index].expire %> 用量比:<%= ret[index].use_percent %>%
日期比:<%= ret[index].date_percent %>%<br>
<% } %>
<% }; %>
</p>
</div>
</body>
</html>

102
views/danmaku.ejs Normal file
View File

@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>主流视频网站弹幕文件解析接口</title>
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery所以必须放在前边) -->
<script src="https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js"></script>
<!-- bootstrap -->
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<!-- Cloudflare Web Analytics -->
<script defer src='https://static.cloudflareinsights.com/beacon.min.js'
data-cf-beacon='{"token": "938fe927c5c44a888fb536713a2f1025"}'></script>
<!-- End Cloudflare Web Analytics -->
</head>
<body>
<div class="container">
<div class="row text-center">
<div class="page-header">
<h1>
主流视频网站弹幕文件解析接口
</h1>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<p>
这是一个弹幕文件解析接口输入你要解析的视频地址即可获得B站弹幕形式的XML文件。<br />
通过使用<a href="https:///www.dandanplay.com/">弹弹Play播放器</a>
或者<a href='https://tiansh.github.io/us-danmaku/bilibili/'>bilibili ASS 弹幕在线转换项目</a>
转换为普通字幕文件,即可在本地播放器中播放。
</p>
<p>
使用方法在当前页面添加一个查询字符串url<br />
目前支持芒果TV腾讯视频优酷视频爱奇艺视频哔哩哔哩。<br />
<strong>温馨提示:点击提交按钮,耐心等待就好,切勿疯狂刷新。</strong><br />
<!-- 会对弹幕文本进行去重去除包含xml标签的非法弹幕文本 -->
例子:<br />
<% urls.forEach(function(url) { %>
<%= path %>?url=<%= url %><br />
<% }); %>
</p>
</div>
<div class="col-sm-4">
</div>
</div>
<div class="row">
<p>在下方直接输入视频网址,点击提交按钮也可解析。</p>
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-1 control-label">视频网址</label>
<div class="col-sm-5">
<input type="text" class="form-control" placeholder="URL" name="url">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-5">
<div class="checkbox">
<label>
<input type="checkbox" name="download" checked='checked'> 强制下载
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-5">
<button type="submit" class="btn btn-primary">提交</button>
</div>
</div>
</form>
</div>
<hr />
<footer class="footer">
<div class="row">
Powered by <a href="https://fly.io/"><strong>Fly.io</strong> </a>
<span class="post-meta-divider">|</span>
Reference blog
<!-- <a href="https://lxmymjr.github.io/contents/%E4%B8%BB%E6%B5%81%E8%A7%86%E9%A2%91%E7%BD%91%E7%AB%99%E5%BC%B9%E5%B9%95%E4%B8%8B%E8%BD%BD">主流视频网站弹幕下载</a>-->
<a href="https://blog.home999.cc/2020/%E5%9F%BA%E4%BA%8E%E9%98%BF%E9%87%8C%E4%BA%91%E5%87%BD%E6%95%B0%E5%AE%9E%E7%8E%B0%E5%BC%B9%E5%B9%95%E6%96%87%E4%BB%B6%E8%A7%A3%E6%9E%90%E6%8E%A5%E5%8F%A3">主流视频网站弹幕下载</a>
</div>
<!--
<div class="row">
今日访问量:{{ getpageinfo.today_visited }}<span class="post-meta-divider">|</span>
昨日访问量:{{ getpageinfo.lastday_visited }}<span class="post-meta-divider">|</span>
当月访问量:{{ getpageinfo.month_visited }}
</div>
-->
</footer>
</div>
</body>
<script>
</script>
</html>

16
views/error.ejs Normal file
View File

@@ -0,0 +1,16 @@
<html>
<head>
<title><%= error.status %> <%= message %></title>
</head>
<body>
<center>
<h1><%= error.status %> <%= message %></h1>
</center>
<hr>
<center>Express</center>
</body>
</html>