Merge pull request #1453 from pan93412/refactor/server-seperation

refactor(server): 使伺服器可以復用、更換廢棄函式
This commit is contained in:
binaryify 2022-01-27 10:49:33 +08:00 committed by GitHub
commit 36942a350c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 562 additions and 189 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ node_modules
.idea .idea
.vscode .vscode
.history .history
examples/moddef.json

146
app.js
View File

@ -1,146 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require('fs') require('./server').serveNcmApi({
const path = require('path') checkVersion: true,
const express = require('express')
const bodyParser = require('body-parser')
const request = require('./util/request')
const packageJSON = require('./package.json')
const exec = require('child_process').exec
const cache = require('./util/apicache').middleware
const { cookieToJson } = require('./util/index')
const fileUpload = require('express-fileupload')
const decode = require('safe-decode-uri-component')
// version check
exec('npm info NeteaseCloudMusicApi version', (err, stdout, stderr) => {
if (!err) {
let version = stdout.trim()
if (packageJSON.version < version) {
console.log(
`最新版本: ${version}, 当前版本: ${packageJSON.version}, 请及时更新`,
)
}
}
}) })
const app = express()
app.set('trust proxy', true)
// CORS & Preflight request
app.use((req, res, next) => {
if (req.path !== '/' && !req.path.includes('.')) {
res.set({
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Origin': req.headers.origin || '*',
'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
'Content-Type': 'application/json; charset=utf-8',
})
}
req.method === 'OPTIONS' ? res.status(204).end() : next()
})
// cookie parser
app.use((req, res, next) => {
req.cookies = {}
//;(req.headers.cookie || '').split(/\s*;\s*/).forEach((pair) => { // Polynomial regular expression //
;(req.headers.cookie || '').split(/;\s+|(?<!\s)\s+$/g).forEach((pair) => {
let crack = pair.indexOf('=')
if (crack < 1 || crack == pair.length - 1) return
req.cookies[decode(pair.slice(0, crack)).trim()] = decode(
pair.slice(crack + 1),
).trim()
})
next()
})
// body parser
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(fileUpload())
// static
app.use(express.static(path.join(__dirname, 'public')))
// cache
app.use(cache('2 minutes', (req, res) => res.statusCode === 200))
// router
const special = {
'daily_signin.js': '/daily_signin',
'fm_trash.js': '/fm_trash',
'personal_fm.js': '/personal_fm',
}
fs.readdirSync(path.join(__dirname, 'module'))
.reverse()
.forEach((file) => {
if (!file.endsWith('.js')) return
let route =
file in special
? special[file]
: '/' + file.replace(/\.js$/i, '').replace(/_/g, '/')
let question = require(path.join(__dirname, 'module', file))
app.use(route, (req, res) => {
;[req.query, req.body].forEach((item) => {
if (typeof item.cookie === 'string') {
item.cookie = cookieToJson(decode(item.cookie))
}
})
let query = Object.assign(
{},
{ cookie: req.cookies },
req.query,
req.body,
req.files,
)
question(query, request)
.then((answer) => {
console.log('[OK]', decode(req.originalUrl))
const cookies = answer.cookie
if (Array.isArray(cookies) && cookies.length > 0) {
if (req.protocol === 'https') {
// Try to fix CORS SameSite Problem
res.append(
'Set-Cookie',
cookies.map((cookie) => {
return cookie + '; SameSite=None; Secure'
}),
)
} else {
res.append('Set-Cookie', cookies)
}
}
res.status(answer.status).send(answer.body)
})
.catch((answer) => {
console.log('[ERR]', decode(req.originalUrl), {
status: answer.status,
body: answer.body,
})
if (!answer.body) {
res.status(404).send({
code: 404,
data: null,
msg: 'Not Found',
})
return
}
if (answer.body.code == '301') answer.body.msg = '需要登录'
res.append('Set-Cookie', answer.cookie)
res.status(answer.status).send(answer.body)
})
})
})
const port = process.env.PORT || 3000
const host = process.env.HOST || ''
app.server = app.listen(port, host, () => {
console.log(`server running @ http://${host ? host : 'localhost'}:${port}`)
})
module.exports = app

View File

@ -1,15 +0,0 @@
const fs = require('fs')
const path = require('path')
let app
before(() => {
app = require('./app.js')
global.host = 'http://localhost:' + app.server.address().port
})
after((done) => {
app.server.close(done)
})
fs.readdirSync(path.join(__dirname, 'test')).forEach((file) => {
require(path.join(__dirname, 'test', file))
})

View File

@ -0,0 +1,22 @@
const fsPromises = require('fs/promises')
const path = require('path')
const server = require('../server')
const exportFile = path.join(__dirname, 'moddef.json')
async function main() {
const def = await server.getModulesDefinitions(
path.join(__dirname, '..', 'module'),
{
'daily_signin.js': '/daily_signin',
'fm_trash.js': '/fm_trash',
'personal_fm.js': '/personal_fm',
},
false,
)
fsPromises.writeFile(exportFile, JSON.stringify(def, null, 4))
console.log(`👍 Get your own definition at: ${exportFile}`)
}
main()

26
main.js
View File

@ -1,26 +0,0 @@
const fs = require('fs')
const path = require('path')
const request = require('./util/request')
const { cookieToJson } = require('./util/index')
let obj = {}
fs.readdirSync(path.join(__dirname, 'module'))
.reverse()
.forEach((file) => {
if (!file.endsWith('.js')) return
let fileModule = require(path.join(__dirname, 'module', file))
obj[file.split('.').shift()] = function (data) {
if (typeof data.cookie === 'string') {
data.cookie = cookieToJson(data.cookie)
}
return fileModule(
{
...data,
cookie: data.cookie ? data.cookie : {},
},
request,
)
}
})
module.exports = obj

201
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "NeteaseCloudMusicApi", "name": "NeteaseCloudMusicApi",
"version": "4.2.0", "version": "4.3.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "NeteaseCloudMusicApi", "name": "NeteaseCloudMusicApi",
"version": "4.2.0", "version": "4.3.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^0.24.0", "axios": "^0.24.0",
@ -24,6 +24,9 @@
"NeteaseCloudMusicApi": "app.js" "NeteaseCloudMusicApi": "app.js"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.13",
"@types/express-fileupload": "^1.2.2",
"@types/mocha": "^9.1.0",
"@types/node": "16.11.19", "@types/node": "16.11.19",
"@typescript-eslint/eslint-plugin": "5.0.0", "@typescript-eslint/eslint-plugin": "5.0.0",
"@typescript-eslint/parser": "5.0.0", "@typescript-eslint/parser": "5.0.0",
@ -186,22 +189,107 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/@types/body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
"dev": true,
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/busboy": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-0.3.2.tgz",
"integrity": "sha512-iEvdm9Z9KdSs/ozuh1Z7ZsXrOl8F4M/CLMXPZHr3QuJ4d6Bjn+HBMC5EMKpwpAo8oi8iK9GZfFoHaIMrrZgwVw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/debug": { "node_modules/@types/debug": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
}, },
"node_modules/@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
"integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==",
"dev": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.18",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-fileupload": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/express-fileupload/-/express-fileupload-1.2.2.tgz",
"integrity": "sha512-sWU1EVFfLsdAginKVrkwTRbRPnbn7dawxEFEBgaRDcpNFCUuksZtASaAKEhqwEIg6fSdeTyI6dIUGl3thhrypg==",
"dev": true,
"dependencies": {
"@types/busboy": "^0",
"@types/express": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.17.28",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz",
"integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.9", "version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true "dev": true
}, },
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
"node_modules/@types/mocha": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz",
"integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==",
"dev": true
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "16.11.19", "version": "16.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz",
"integrity": "sha512-BPAcfDPoHlRQNKktbsbnpACGdypPFBuX4xQlsWDE7B8XXcfII+SpOLay3/qZmCLb39kV5S1RTYwXdkx2lwLYng==" "integrity": "sha512-BPAcfDPoHlRQNKktbsbnpACGdypPFBuX4xQlsWDE7B8XXcfII+SpOLay3/qZmCLb39kV5S1RTYwXdkx2lwLYng=="
}, },
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"node_modules/@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"node_modules/@types/readable-stream": { "node_modules/@types/readable-stream": {
"version": "2.3.9", "version": "2.3.9",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.9.tgz", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.9.tgz",
@ -211,6 +299,16 @@
"safe-buffer": "*" "safe-buffer": "*"
} }
}, },
"node_modules/@types/serve-static": {
"version": "1.13.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
"integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==",
"dev": true,
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.0.0.tgz",
@ -1502,6 +1600,8 @@
"resolved": "https://registry.npm.taobao.org/enquirer/download/enquirer-2.3.6.tgz?cache=0&sync_timestamp=1593693291943&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fenquirer%2Fdownload%2Fenquirer-2.3.6.tgz", "resolved": "https://registry.npm.taobao.org/enquirer/download/enquirer-2.3.6.tgz?cache=0&sync_timestamp=1593693291943&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fenquirer%2Fdownload%2Fenquirer-2.3.6.tgz",
"integrity": "sha1-Kn/l3WNKHkElqXXsmU/1RW3Dc00=", "integrity": "sha1-Kn/l3WNKHkElqXXsmU/1RW3Dc00=",
"dev": true, "dev": true,
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"ansi-colors": "^4.1.1" "ansi-colors": "^4.1.1"
}, },
@ -5770,22 +5870,107 @@
"resolved": "https://registry.npm.taobao.org/@tootallnate/once/download/@tootallnate/once-1.1.2.tgz", "resolved": "https://registry.npm.taobao.org/@tootallnate/once/download/@tootallnate/once-1.1.2.tgz",
"integrity": "sha1-zLkURTYBeaBOf+av94wA/8Hur4I=" "integrity": "sha1-zLkURTYBeaBOf+av94wA/8Hur4I="
}, },
"@types/body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/busboy": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-0.3.2.tgz",
"integrity": "sha512-iEvdm9Z9KdSs/ozuh1Z7ZsXrOl8F4M/CLMXPZHr3QuJ4d6Bjn+HBMC5EMKpwpAo8oi8iK9GZfFoHaIMrrZgwVw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/debug": { "@types/debug": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
}, },
"@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
"integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.18",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-fileupload": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/express-fileupload/-/express-fileupload-1.2.2.tgz",
"integrity": "sha512-sWU1EVFfLsdAginKVrkwTRbRPnbn7dawxEFEBgaRDcpNFCUuksZtASaAKEhqwEIg6fSdeTyI6dIUGl3thhrypg==",
"dev": true,
"requires": {
"@types/busboy": "^0",
"@types/express": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.28",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz",
"integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.9", "version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true "dev": true
}, },
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
"@types/mocha": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz",
"integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==",
"dev": true
},
"@types/node": { "@types/node": {
"version": "16.11.19", "version": "16.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz",
"integrity": "sha512-BPAcfDPoHlRQNKktbsbnpACGdypPFBuX4xQlsWDE7B8XXcfII+SpOLay3/qZmCLb39kV5S1RTYwXdkx2lwLYng==" "integrity": "sha512-BPAcfDPoHlRQNKktbsbnpACGdypPFBuX4xQlsWDE7B8XXcfII+SpOLay3/qZmCLb39kV5S1RTYwXdkx2lwLYng=="
}, },
"@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"@types/readable-stream": { "@types/readable-stream": {
"version": "2.3.9", "version": "2.3.9",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.9.tgz", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.9.tgz",
@ -5795,6 +5980,16 @@
"safe-buffer": "*" "safe-buffer": "*"
} }
}, },
"@types/serve-static": {
"version": "1.13.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
"integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==",
"dev": true,
"requires": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.0.0.tgz",
@ -6743,6 +6938,8 @@
"resolved": "https://registry.npm.taobao.org/enquirer/download/enquirer-2.3.6.tgz?cache=0&sync_timestamp=1593693291943&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fenquirer%2Fdownload%2Fenquirer-2.3.6.tgz", "resolved": "https://registry.npm.taobao.org/enquirer/download/enquirer-2.3.6.tgz?cache=0&sync_timestamp=1593693291943&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fenquirer%2Fdownload%2Fenquirer-2.3.6.tgz",
"integrity": "sha1-Kn/l3WNKHkElqXXsmU/1RW3Dc00=", "integrity": "sha1-Kn/l3WNKHkElqXXsmU/1RW3Dc00=",
"dev": true, "dev": true,
"optional": true,
"peer": true,
"requires": { "requires": {
"ansi-colors": "^4.1.1" "ansi-colors": "^4.1.1"
} }

View File

@ -4,7 +4,7 @@
"description": "网易云音乐 NodeJS 版 API", "description": "网易云音乐 NodeJS 版 API",
"scripts": { "scripts": {
"start": "node app.js", "start": "node app.js",
"test": "mocha -r intelli-espower-loader -t 20000 app.test.js --exit", "test": "mocha -r intelli-espower-loader -t 20000 server.test.js --exit",
"lint": "eslint \"**/*.{js,ts}\"", "lint": "eslint \"**/*.{js,ts}\"",
"lint-fix": "eslint --fix \"**/*.{js,ts}\"", "lint-fix": "eslint --fix \"**/*.{js,ts}\"",
"prepare": "husky install", "prepare": "husky install",
@ -22,7 +22,7 @@
"音乐", "音乐",
"网易云音乐nodejs" "网易云音乐nodejs"
], ],
"main": "main.js", "main": "server.js",
"types": "./interface.d.ts", "types": "./interface.d.ts",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -56,6 +56,9 @@
"yargs": "^17.1.1" "yargs": "^17.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.13",
"@types/express-fileupload": "^1.2.2",
"@types/mocha": "^9.1.0",
"@types/node": "16.11.19", "@types/node": "16.11.19",
"@typescript-eslint/eslint-plugin": "5.0.0", "@typescript-eslint/eslint-plugin": "5.0.0",
"@typescript-eslint/parser": "5.0.0", "@typescript-eslint/parser": "5.0.0",

300
server.js Normal file
View File

@ -0,0 +1,300 @@
const fs = require('fs')
const path = require('path')
const express = require('express')
const request = require('./util/request')
const packageJSON = require('./package.json')
const exec = require('child_process').exec
const cache = require('./util/apicache').middleware
const { cookieToJson } = require('./util/index')
const fileUpload = require('express-fileupload')
const decode = require('safe-decode-uri-component')
/**
* The version check result.
* @readonly
* @enum {number}
*/
const VERSION_CHECK_RESULT = {
FAILED: -1,
NOT_LATEST: 0,
LATEST: 1,
}
/**
* @typedef {{
* identifier?: string,
* route: string,
* module: any
* }} ModuleDefinition
*/
/**
* @typedef {{
* port?: number,
* host?: string,
* checkVersion?: boolean,
* moduleDefs?: ModuleDefinition[]
* }} NcmApiOptions
*/
/**
* @typedef {{
* status: VERSION_CHECK_RESULT,
* ourVersion?: string,
* npmVersion?: string,
* }} VersionCheckResult
*/
/**
* @typedef {{
* server?: import('http').Server,
* }} ExpressExtension
*/
/**
* Get the module definitions dynamically.
*
* @param {string} modulesPath The path to modules (JS).
* @param {Record<string, string>} [specificRoute] The specific route of specific modules.
* @param {boolean} [doRequire] If true, require() the module directly.
* Otherwise, print out the module path. Default to true.
* @returns {Promise<ModuleDefinition[]>} The module definitions.
*
* @example getModuleDefinitions("./module", {"album_new.js": "/album/create"})
*/
async function getModulesDefinitions(
modulesPath,
specificRoute,
doRequire = true,
) {
const files = await fs.promises.readdir(modulesPath)
const parseRoute = (/** @type {string} */ fileName) =>
specificRoute && fileName in specificRoute
? specificRoute[fileName]
: `/${fileName.replace(/\.js$/i, '').replace(/_/g, '/')}`
const modules = files
.reverse()
.filter((file) => file.endsWith('.js'))
.map((file) => {
const identifier = file.split('.').shift()
const route = parseRoute(file)
const modulePath = path.join(modulesPath, file)
const module = doRequire ? require(modulePath) : modulePath
return { identifier, route, module }
})
return modules
}
/**
* Check if the version of this API is latest.
*
* @returns {Promise<VersionCheckResult>} If true, this API is up-to-date;
* otherwise, this API should be upgraded and you would
* need to notify users to upgrade it manually.
*/
async function checkVersion() {
return new Promise((resolve) => {
exec('npm info NeteaseCloudMusicApi version', (err, stdout) => {
if (!err) {
let version = stdout.trim()
/**
* @param {VERSION_CHECK_RESULT} status
*/
const resolveStatus = (status) =>
resolve({
status,
ourVersion: packageJSON.version,
npmVersion: version,
})
resolveStatus(
packageJSON.version < version
? VERSION_CHECK_RESULT.NOT_LATEST
: VERSION_CHECK_RESULT.LATEST,
)
}
})
resolve({
status: VERSION_CHECK_RESULT.FAILED,
})
})
}
/**
* Construct the server of NCM API.
*
* @param {ModuleDefinition[]} [moduleDefs] Customized module definitions [advanced]
* @returns {Promise<import("express").Express>} The server instance.
*/
async function consturctServer(moduleDefs) {
const app = express()
app.set('trust proxy', true)
/**
* CORS & Preflight request
*/
app.use((req, res, next) => {
if (req.path !== '/' && !req.path.includes('.')) {
res.set({
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Origin': req.headers.origin || '*',
'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
'Content-Type': 'application/json; charset=utf-8',
})
}
req.method === 'OPTIONS' ? res.status(204).end() : next()
})
/**
* Cookie Parser
*/
app.use((req, _, next) => {
req.cookies = {}
//;(req.headers.cookie || '').split(/\s*;\s*/).forEach((pair) => { // Polynomial regular expression //
;(req.headers.cookie || '').split(/;\s+|(?<!\s)\s+$/g).forEach((pair) => {
let crack = pair.indexOf('=')
if (crack < 1 || crack == pair.length - 1) return
req.cookies[decode(pair.slice(0, crack)).trim()] = decode(
pair.slice(crack + 1),
).trim()
})
next()
})
/**
* Body Parser and File Upload
*/
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(fileUpload())
/**
* Serving static files
*/
app.use(express.static(path.join(__dirname, 'public')))
/**
* Cache
*/
app.use(cache('2 minutes', (_, res) => res.statusCode === 200))
/**
* Special Routers
*/
const special = {
'daily_signin.js': '/daily_signin',
'fm_trash.js': '/fm_trash',
'personal_fm.js': '/personal_fm',
}
/**
* Load every modules in this directory
*/
const moduleDefinitions =
moduleDefs ||
(await getModulesDefinitions(path.join(__dirname, 'module'), special))
for (const moduleDef of moduleDefinitions) {
// Register the route.
app.use(moduleDef.route, async (req, res) => {
;[req.query, req.body].forEach((item) => {
if (typeof item.cookie === 'string') {
item.cookie = cookieToJson(decode(item.cookie))
}
})
let query = Object.assign(
{},
{ cookie: req.cookies },
req.query,
req.body,
)
try {
const moduleResponse = await moduleDef.module(query, request)
console.log('[OK]', decode(req.originalUrl))
const cookies = moduleResponse.cookie
if (Array.isArray(cookies) && cookies.length > 0) {
if (req.protocol === 'https') {
// Try to fix CORS SameSite Problem
res.append(
'Set-Cookie',
cookies.map((cookie) => {
return cookie + '; SameSite=None; Secure'
}),
)
} else {
res.append('Set-Cookie', cookies)
}
}
res.status(moduleResponse.status).send(moduleResponse.body)
} catch (/** @type {*} */ moduleResponse) {
console.log('[ERR]', decode(req.originalUrl), {
status: moduleResponse.status,
body: moduleResponse.body,
})
if (!moduleResponse.body) {
res.status(404).send({
code: 404,
data: null,
msg: 'Not Found',
})
return
}
if (moduleResponse.body.code == '301')
moduleResponse.body.msg = '需要登录'
res.append('Set-Cookie', moduleResponse.cookie)
res.status(moduleResponse.status).send(moduleResponse.body)
}
})
}
return app
}
/**
* Serve the NCM API.
* @param {NcmApiOptions} options
* @returns {Promise<import('express').Express & ExpressExtension>}
*/
async function serveNcmApi(options) {
const port = Number(options.port || process.env.PORT || '3000')
const host = options.host || process.env.HOST || ''
const checkVersionSubmission =
options.checkVersion &&
checkVersion().then(({ npmVersion, ourVersion, status }) => {
if (status == VERSION_CHECK_RESULT.NOT_LATEST) {
console.log(
`最新版本: ${npmVersion}, 当前版本: ${ourVersion}, 请及时更新`,
)
}
})
const constructServerSubmission = consturctServer(options.moduleDefs)
const [_, app] = await Promise.all([
checkVersionSubmission,
constructServerSubmission,
])
/** @type {import('express').Express & ExpressExtension} */
const appExt = app
appExt.server = app.listen(port, host, () => {
console.log(`server running @ http://${host ? host : 'localhost'}:${port}`)
})
return appExt
}
module.exports = {
serveNcmApi,
getModulesDefinitions,
}

33
server.test.js Normal file
View File

@ -0,0 +1,33 @@
const fs = require('fs')
const path = require('path')
const serverMod = require('./server')
/** @type {import("express").Express & serverMod.ExpressExtension} */
let app
before(async () => {
app = await serverMod.serveNcmApi({})
if (app.server && app.server.address) {
const addr = app.server.address()
if (addr && typeof addr === 'object' && 'port' in addr) {
global.host = `http://localhost:${addr.port}`
return
}
}
throw new Error('failed to set up host')
})
after((done) => {
if (app.server) {
app.server.close(done)
return
}
throw new Error('failed to set up server')
})
fs.readdirSync(path.join(__dirname, 'test')).forEach((file) => {
require(path.join(__dirname, 'test', file))
})