mirror of
https://github.com/lyz05/danmaku.git
synced 2026-02-02 17:59:53 +08:00
feat: first commit
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
.git
|
||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
12
.idea/danmaku.iml
generated
Normal file
12
.idea/danmaku.iml
generated
Normal 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
6
.idea/jsLibraryMappings.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
32
Dockerfile
Normal 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
23
README.md
Normal 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
49
app.js
Normal 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
90
bin/www
Normal 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
39
fly.toml
Normal 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
9551
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
8
public/stylesheets/style.css
Normal file
8
public/stylesheets/style.css
Normal 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
150
routes/airportsub.js
Normal 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
14
routes/api/base.js
Normal 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
97
routes/api/bilibili.js
Normal 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
95
routes/api/iqiyi.js
Normal 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
75
routes/api/mgtv.js
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
83
routes/api/tencentvideo.js
Normal file
83
routes/api/tencentvideo.js
Normal 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
50
routes/api/utils.js
Normal 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
149
routes/api/youku.js
Normal 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
53
routes/danmaku.js
Normal 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
32
routes/ipinfo.js
Normal 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
9
routes/users.js
Normal 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
26
test.js
Normal 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
80
test/App.test.js
Normal 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
58
utils/oss.js
Normal 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
74
views/airportdownload.ejs
Normal 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
45
views/airportsub.ejs
Normal 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&port=443&secret=dddd561961fea026e517764b084bd64072">域名</a>
|
||||
<a href="tg://proxy?server=167.235.77.32&port=443&secret=dddd561961fea026e517764b084bd64072">IPV4</a>
|
||||
<a href="tg://proxy?server=2a01:4f8:1c1e:eeb6::1&port=443&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
102
views/danmaku.ejs
Normal 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
16
views/error.ejs
Normal 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>
|
||||
Reference in New Issue
Block a user