From ce24a65ca1535b4592a5e9f03de95839a259c414 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Mon, 3 Jul 2017 11:35:07 +0800 Subject: [PATCH] first commit --- .gitignore | 2 + Dockerfile | 5 ++ README.md | 43 +++++++++++++ blacklist | 3 + docker-compose.yml | 32 ++++++++++ index.js | 14 ++++ logs/.gitkeep | 0 models/danmaku.js | 16 +++++ package.json | 18 ++++++ routes/all.js | 17 +++++ routes/bilibili.js | 134 +++++++++++++++++++++++++++++++++++++++ routes/get.js | 43 +++++++++++++ routes/list.js | 17 +++++ routes/post.js | 118 ++++++++++++++++++++++++++++++++++ routes/video-bilibili.js | 72 +++++++++++++++++++++ tools/logger.js | 20 ++++++ tools/mongodb.js | 11 ++++ tools/redis.js | 26 ++++++++ 18 files changed, 591 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 blacklist create mode 100644 docker-compose.yml create mode 100644 index.js create mode 100644 logs/.gitkeep create mode 100644 models/danmaku.js create mode 100644 package.json create mode 100644 routes/all.js create mode 100644 routes/bilibili.js create mode 100644 routes/get.js create mode 100644 routes/list.js create mode 100644 routes/post.js create mode 100644 routes/video-bilibili.js create mode 100644 tools/logger.js create mode 100644 tools/mongodb.js create mode 100644 tools/redis.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81f35fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +logs/DPlayer.log* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ad613a8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM node:6-onbuild +WORKDIR /usr/src/app +RUN npm install pm2 -g +EXPOSE 1207 +CMD pm2-docker index.js \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..eda0e0c --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# DPlayer backend + +> Node.js backend for [DPlayer](https://github.com/DIYgod/DPlayer) + +## Usage + +``` +$ docker-compose build +$ docker-compose pull +$ docker-compose up +``` + +## Communication Groups + +[Telegram Group](https://t.me/adplayer) + +[QQ Group: 415835947](https://shang.qq.com/wpa/qunwpa?idkey=bf22213ae0028a82e5adf3f286dfd4f01e0997dc9f1dcd8e831a0a85e799be17) + +## Related Projects + +- [DPlayer](https://github.com/DIYgod/DPlayer) + +- [Official danmaku data (https://api.prprpr.me/dplayer/)](https://github.com/DIYgod/DPlayer-data) + +- [DPlayer-for-typecho](https://github.com/volio/DPlayer-for-typecho) + +- [Hexo-tag-dplayer](https://github.com/NextMoe/hexo-tag-dplayer) + +- [DPlayer_for_Z-BlogPHP](https://github.com/fghrsh/DPlayer_for_Z-BlogPHP) + +- [纸飞机视频区插件(DPlayer for Discuz!)](https://coding.net/u/Click_04/p/video/git) + +- [dplayer_py_backend](https://github.com/dixyes/dplayer_py_backend) + +- [dplayer_lua_backend](https://github.com/dixyes/dplayer_lua_backend) + +- [DPlayer for WordPress](https://github.com/BlueCocoa/DPlayer-WordPress) + +- [Vue-DPlayer](https://github.com/sinchang/vue-dplayer) + +## LICENSE + +[The Star And Thank Author License (SATA)](https://github.com/DIYgod/DPlayer/blob/master/LICENSE) \ No newline at end of file diff --git a/blacklist b/blacklist new file mode 100644 index 0000000..1d38074 --- /dev/null +++ b/blacklist @@ -0,0 +1,3 @@ +Can be username and IP +username +0.0.0.0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..83818ec --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '2.1' + +services: + mongo: + image: mongo:latest + ports: + - 27017:27017 + volumes: + - ~/dplayer/db:/data/db + redis: + image: redis:latest + ports: + - "6379:6379" + web: + build: . + links: + - mongo + - redis + depends_on: + - mongo + - redis + ports: + - 1207:1207 + environment: + REDIS_PORT_6379_TCP_ADDR: "redis" + REDIS_PORT_6379_TCP_PORT: 6379 + MONGO_PORT_27017_TCP_ADDR: "mongo" + MONGO_PORT_27017_TCP_PORT: 27017 + MONGO_INSTANCE_NAME: "danmaku" + volumes: + - ~/dplayer/logs:/usr/src/app/logs + - ~/dplayer/pm2logs:/root/.pm2/logs \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..7c010f0 --- /dev/null +++ b/index.js @@ -0,0 +1,14 @@ +var express = require('express'); +var logger = require('./tools/logger'); +require('./tools/mongodb'); + +logger.info(`🍻 DPlayer start! Cheers!`); + +var app = express(); +app.all('*', require('./routes/all')); +app.get('/', require('./routes/get')); +app.post('/', require('./routes/post')); +app.get('/list', require('./routes/list')); +app.get('/bilibili', require('./routes/bilibili')); +app.get('/video/bilibili', require('./routes/video-bilibili')); +app.listen(1207); \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/models/danmaku.js b/models/danmaku.js new file mode 100644 index 0000000..6b0512b --- /dev/null +++ b/models/danmaku.js @@ -0,0 +1,16 @@ +var mongoose = require('../tools/mongodb'); +var danmakuSchema = new mongoose.Schema({ + player: { + type: [String], index: true + }, + author: String, + time: Number, + text: String, + color: String, + type: String, + ip: String, + referer: String +}); +var danmaku = mongoose.model('dan', danmakuSchema); + +module.exports = danmaku; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..9ab3b24 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "DPlayer_nodejs", + "version": "0.0.1", + "description": "", + "main": "index.js", + "author": "DIYgod", + "license": "MIT", + "devDependencies": {}, + "dependencies": { + "blueimp-md5": "^2.4.0", + "express": "^4.13.4", + "log4js": "^0.6.36", + "mongoose": "^4.1.9", + "node-fetch": "^1.6.3", + "redis": "^2.6.2", + "xml2js": "^0.4.17" + } +} diff --git a/routes/all.js b/routes/all.js new file mode 100644 index 0000000..14f97bd --- /dev/null +++ b/routes/all.js @@ -0,0 +1,17 @@ +var https = require('https'); + +module.exports = function (req, res, next) { + https.get(`https://api.prprpr.me/count/?id=DIYgod-DPlayer&action=add`); + + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild'); + res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); + res.header('Cache-control', 'no-cache'); + + if (req.method == 'OPTIONS') { + res.send(200); + } + else { + next(); + } +}; \ No newline at end of file diff --git a/routes/bilibili.js b/routes/bilibili.js new file mode 100644 index 0000000..296290d --- /dev/null +++ b/routes/bilibili.js @@ -0,0 +1,134 @@ +var url = require('url'); +var logger = require('../tools/logger'); +var redis = require('../tools/redis'); +var fetch = require('node-fetch'); +var parseString = require('xml2js').parseString; + +module.exports = function (req, res) { + res.header('content-type', 'application/json; charset=utf-8'); + + var ip = req.headers['x-forwarded-for'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket.remoteAddress; + + var query = url.parse(req.url,true).query; + var aid = query.aid; + var cid = query.cid; + + function addZero(str, length){ + return new Array(Math.max(length - str.length + 1, 0)).join("0") + str; + } + + if (cid) { + redis.client.get(`bilibilicid2dan${cid}`, function(err, reply) { + if (reply) { + logger.info(`Bilibili cid2dan ${cid} form redis, IP: ${ip}`); + res.send(reply); + } + else { + logger.info(`Bilibili cid2dan ${cid} form origin, IP: ${ip}`); + + var dan = { + code: 1, + danmaku: [] + }; + + fetch(`http://comment.bilibili.com/${cid}.xml`).then( + response => response.text() + ).then((data) => { + parseString(data, function (err, result) { + var danOriginal = result.i.d; + for (var i = 0; i < danOriginal.length; i++) { + var info = danOriginal[i].$.p.split(','); + var type = ''; + if (info[1] === '4') { + type = 'bottom'; + } + else if (info[1] === '5') { + type = 'top'; + } + else { + type = 'right'; + } + var danOne = { + author: 'bilibili' + info[6], + time: info[0], + text: danOriginal[i]._, + color: '#' + addZero(parseInt(info[3]).toString(16), 6), + type: type + }; + dan.danmaku.push(danOne); + } + var sendDan = JSON.stringify(dan); + res.send(sendDan); + + redis.set(`bilibilicid2dan${cid}`, sendDan); + }); + } + ).catch( + e => logger.error("Bilibilib Error: getting danmaku", e) + ); + } + }); + } + else { + redis.client.get(`bilibiliaid2dan${aid}`, function(err, reply) { + if (reply) { + logger.info(`Bilibili aid2dan ${aid} form redis, IP: ${ip}`); + res.send(reply); + } + else { + logger.info(`Bilibili aid2dan ${aid} form origin, IP: ${ip}`); + + var dan = { + code: 1, + danmaku: [] + }; + + fetch(`http://www.bilibili.com/widget/getPageList?aid=${aid}`).then( + response => response.json() + ).then((data) => { + fetch(`http://comment.bilibili.com/${data[0].cid}.xml`).then( + response => response.text() + ).then((data) => { + parseString(data, function (err, result) { + var danOriginal = result.i.d; + for (var i = 0; i < danOriginal.length; i++) { + var info = danOriginal[i].$.p.split(','); + var type = ''; + if (info[1] === '4') { + type = 'bottom'; + } + else if (info[1] === '5') { + type = 'top'; + } + else { + type = 'right'; + } + var danOne = { + author: 'bilibili' + info[6], + time: info[0], + text: danOriginal[i]._, + color: '#' + addZero(parseInt(info[3]).toString(16), 6), + type: type + }; + dan.danmaku.push(danOne); + } + var sendDan = JSON.stringify(dan); + res.send(sendDan); + + redis.set(`bilibiliaid2dan${aid}`, sendDan); + }); + } + ).catch( + e => logger.error("Bilibilib Error: getting danmaku", e) + ); + } + ).catch( + e => logger.error("Bilibilib Error: getting cid", e) + ); + } + }); + } +}; \ No newline at end of file diff --git a/routes/get.js b/routes/get.js new file mode 100644 index 0000000..7b1160b --- /dev/null +++ b/routes/get.js @@ -0,0 +1,43 @@ +var url = require('url'); +var logger = require('../tools/logger'); +var danmaku = require('../models/danmaku'); +var redis = require('../tools/redis'); + +module.exports = function (req, res) { + res.header('content-type', 'application/json; charset=utf-8'); + + var ip = req.headers['x-forwarded-for'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket.remoteAddress; + + var query = url.parse(req.url,true).query; + var id = query.id; + var max = query.max; + + redis.client.get(`dplayer${id}`, function(err, reply) { + if (reply) { + logger.info(`DPlayer id ${id} form redis, IP: ${ip}`); + res.send(reply); + } + else { + logger.info(`DPlayer id ${id} form mongodb, IP: ${ip}`); + + danmaku.find({player: id}, function (err, data) { + if (err) { + logger.error(err); + } + + var dan = { + code: 1, + danmaku: [] + }; + dan.danmaku = max ? data.slice(0, max) : data; + var sendDan = JSON.stringify(dan); + res.send(sendDan); + + redis.set(`dplayer${id}`, sendDan); + }) + } + }); +}; \ No newline at end of file diff --git a/routes/list.js b/routes/list.js new file mode 100644 index 0000000..1f4146e --- /dev/null +++ b/routes/list.js @@ -0,0 +1,17 @@ +var url = require('url'); +var logger = require('../tools/logger'); +var danmaku = require('../models/danmaku'); + +module.exports = function (req, res) { + danmaku.distinct('player', function (err, data) { + if (err) { + logger.error(err); + } + + var json = ``; + for (var i = 0; i < data.length; i++) { + json += data[i] + `
`; + } + res.send(json); + }) +}; \ No newline at end of file diff --git a/routes/post.js b/routes/post.js new file mode 100644 index 0000000..dd3dd8f --- /dev/null +++ b/routes/post.js @@ -0,0 +1,118 @@ +var fs = require('fs'); +var logger = require('../tools/logger'); +var danmaku = require('../models/danmaku'); +var redis = require('../tools/redis'); + +function htmlEncode(str) { + return str.replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/\//g, "/"); +} + +var postIP = []; + +module.exports = function (req, res) { + var body = ''; + var jsonStr = {}; + var ip = req.headers['x-forwarded-for'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket.remoteAddress; + + // check black ip + var blanklist = fs.readFileSync('blacklist').toString().split('\n'); + if (blanklist.indexOf(ip.split(',')[0]) !== -1) { + logger.info(`Reject POST form ${ip} for black ip.`); + res.send(`{"code": -1, "msg": "Rejected for black ip."}`); + return; + } + + // frequency limitation + if (postIP.indexOf(ip) !== -1) { + logger.info(`Reject POST form ${ip} for frequent operation.`); + res.send(`{"code": -2, "msg": "Rejected for frequent operation."}`); + return; + } + else { + postIP.push(ip); + setTimeout(function () { + postIP.splice(0, 1); + }, 1000); + } + + req.on('data', dataListener); + req.on('end', endListener); + + function dataListener (chunk) { + body += chunk; + } + function endListener () { + cleanListener(); + try { + jsonStr = JSON.parse(body); + } catch (err) { + jsonStr = {}; + } + + // check data + if (jsonStr.player === undefined + || jsonStr.author === undefined + || jsonStr.time === undefined + || jsonStr.text === undefined + || jsonStr.color === undefined + || jsonStr.type === undefined + || jsonStr.text.length >= 30) { + logger.info(`Reject POST form ${ip} for illegal data: ${JSON.stringify(jsonStr)}`); + res.send(`{"code": -3, "msg": "Rejected for illegal data"}`); + return; + } + + // check token: set it yourself + function checkToken (token) { + return true; + } + if (!checkToken(jsonStr.token)) { + logger.info(`Rejected POST form ${ip} for illegal token: ${jsonStr.token}`); + res.send(`{"code": -4, "msg": "Rejected for illegal token: ${jsonStr.token}"}`); + return; + } + + // check black username + if (blanklist.indexOf(jsonStr.author) !== -1) { + logger.info(`Reject POST form ${jsonStr.author} for black user.`); + res.send(`{"code": -5, "msg": "Rejected for black user."}`); + return; + } + + logger.info(`POST form ${ip}, data: ${JSON.stringify(jsonStr)}`); + + var dan = new danmaku({ + player: htmlEncode(jsonStr.player), + author: htmlEncode(jsonStr.author), + time: jsonStr.time, + text: htmlEncode(jsonStr.text), + color: htmlEncode(jsonStr.color), + type: htmlEncode(jsonStr.type), + ip: ip, + referer: req.headers.referer + }); + dan.save(function (err, d) { + if (err) { + logger.error(err); + res.send(`{"code": 0, "msg": "Error happens, please contact system administrator."}`); + } + else { + res.send(`{"code": 1, "data": ${JSON.stringify(d)}}`); + redis.client.del(`dplayer${htmlEncode(jsonStr.player)}`); + } + }); + } + + function cleanListener () { + req.removeListener('data', dataListener); + req.removeListener('end', endListener); + } +}; \ No newline at end of file diff --git a/routes/video-bilibili.js b/routes/video-bilibili.js new file mode 100644 index 0000000..b418e88 --- /dev/null +++ b/routes/video-bilibili.js @@ -0,0 +1,72 @@ +var url = require('url'); +var logger = require('../tools/logger'); +var redis = require('../tools/redis'); +var fetch = require('node-fetch'); +var md5 = require('blueimp-md5'); +var xml2js = require('xml2js'); +var parseString = xml2js.parseString; + +var appkey = 'f3bb208b3d081dc8'; +var secret = '1c15888dc316e05a15fdd0a02ed6584f'; +function getData(cid, res, type) { + var para = `cid=${cid}&from=miniplay&player=1&quality=2&type=mp4`; + var sign = md5(`${para}${secret}`); + var api = `http://interface.bilibili.com/playurl?${para}&sign=${sign}`; + + if (type === '1') { + res.send(api); + } + else { + fetch(api, { + headers: {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36'}, + }).then( + response => response.text() + ).then((data) => { + parseString(data, { explicitArray: false }, function (err, result) { + // res.send(result.video.durl.url.replace('http://', 'https://')); + res.redirect(301, result.video.durl.url.replace('http://', 'https://')); + }); + } + ).catch( + e => logger.error("Bilibilib Error: getting data", e) + ); + } +} + +module.exports = function (req, res) { + var ip = req.headers['x-forwarded-for'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket.remoteAddress; + + var query = url.parse(req.url,true).query; + var aid = query.aid; + var cid = query.cid; + var type = query.type; + + if (cid) { + logger.info(`Bilibili cid2video ${cid}, IP: ${ip}`); + getData(cid, res, type); + } + else { + redis.client.get(`bilibiliaid2cid${aid}`, function(err, reply) { + if (reply) { + logger.info(`Bilibili aid2video ${aid} form redis, IP: ${ip}`); + getData(reply, res, type); + } + else { + logger.info(`Bilibili aid2video ${aid} form origin, IP: ${ip}`); + + fetch(`http://www.bilibili.com/widget/getPageList?aid=${aid}`).then( + response => response.json() + ).then((data) => { + redis.set(`bilibiliaid2cid${aid}`, data[0].cid); + getData(data[0].cid, res, type); + } + ).catch( + e => logger.error("Bilibili aid2video Error: getting cid", e) + ); + } + }); + } +}; \ No newline at end of file diff --git a/tools/logger.js b/tools/logger.js new file mode 100644 index 0000000..fb57c59 --- /dev/null +++ b/tools/logger.js @@ -0,0 +1,20 @@ +var log4js = require('log4js'); +log4js.configure({ + appenders: [ + { + type: "file", + filename: 'logs/DPlayer.log', + maxLogSize: 20480, + backups: 3, + category: [ 'DPlayer','console' ] + }, + { + type: "console" + } + ], + replaceConsole: true +}); +var logger = log4js.getLogger('DPlayer'); +logger.setLevel('INFO'); + +module.exports = logger; \ No newline at end of file diff --git a/tools/mongodb.js b/tools/mongodb.js new file mode 100644 index 0000000..0844d70 --- /dev/null +++ b/tools/mongodb.js @@ -0,0 +1,11 @@ +var mongoose = require('mongoose'); +var mongodbUrl; +if (process.env.MONGO_PORT_27017_TCP_ADDR && process.env.MONGO_PORT_27017_TCP_PORT && process.env.MONGO_INSTANCE_NAME) { + mongodbUrl = 'mongodb://' + process.env.MONGO_PORT_27017_TCP_ADDR + ':' + process.env.MONGO_PORT_27017_TCP_PORT + '/' + process.env.MONGO_INSTANCE_NAME; +} +else { + mongodbUrl = 'mongodb://127.0.0.1:27017/danmaku'; +} +mongoose.connect(mongodbUrl); + +module.exports = mongoose; \ No newline at end of file diff --git a/tools/redis.js b/tools/redis.js new file mode 100644 index 0000000..145c096 --- /dev/null +++ b/tools/redis.js @@ -0,0 +1,26 @@ +var logger = require('./logger'); +var redis = require("redis"); +var client; +if (process.env.REDIS_PORT_6379_TCP_ADDR && process.env.REDIS_PORT_6379_TCP_PORT) { + client = redis.createClient({ + host: process.env.REDIS_PORT_6379_TCP_ADDR, + port: process.env.REDIS_PORT_6379_TCP_PORT + }); +} +else { + client = redis.createClient(); +} + + +client.on("error", function (err) { + logger.error('Redis Error ' + err); +}); + +module.exports = { + set: function (key, value) { + client.set(key, value, redis.print); + client.expire(key, 86400); + logger.info('Set redis: ' + key); + }, + client: client +}; \ No newline at end of file