From 7985690987e01c8d67a9404e19fcb32f455d760f Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 11:29:07 +0800 Subject: [PATCH 01/10] refactor(server): separate server from app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 我們需要 NCM API 的伺服器部分,但 app.js 目前的做法讓我們不太好單獨啟動伺服器。 我將 app.js 的伺服器部分抽成各個函數,並將原有 會 blocking 的函數更改為非同步函式。 這樣不僅能最大化善用 Node.js 的 Event Loop, 亦能提升未來維護的程式碼易讀性。 --- app.js | 146 +------------------------ package-lock.json | 8 +- server.js | 263 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+), 146 deletions(-) create mode 100644 server.js diff --git a/app.js b/app.js index 26e657ae48d..3e5e976d2d0 100644 --- a/app.js +++ b/app.js @@ -1,146 +1,4 @@ #!/usr/bin/env node -const fs = require('fs') -const path = require('path') -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}, 请及时更新`, - ) - } - } +require('./server')({ + checkVersion: true, }) - -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+|(? { - 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 diff --git a/package-lock.json b/package-lock.json index 09bb2b62bd6..227a6eec637 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "NeteaseCloudMusicApi", - "version": "4.2.0", + "version": "4.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "NeteaseCloudMusicApi", - "version": "4.2.0", + "version": "4.3.0", "license": "MIT", "dependencies": { "axios": "^0.24.0", @@ -1502,6 +1502,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", "integrity": "sha1-Kn/l3WNKHkElqXXsmU/1RW3Dc00=", "dev": true, + "optional": true, + "peer": true, "dependencies": { "ansi-colors": "^4.1.1" }, @@ -6743,6 +6745,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", "integrity": "sha1-Kn/l3WNKHkElqXXsmU/1RW3Dc00=", "dev": true, + "optional": true, + "peer": true, "requires": { "ansi-colors": "^4.1.1" } diff --git a/server.js b/server.js new file mode 100644 index 00000000000..4c52653fc34 --- /dev/null +++ b/server.js @@ -0,0 +1,263 @@ +#!/usr/bin/env node +const fs = require('fs') +const path = require('path') +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') + +/** + * The version check result. + * @readonly + * @enum {number} + */ +const VERSION_CHECK_RESULT = { + FAILED: -1, + NOT_LATEST: 0, + LATEST: 1, +} + +/** + * @typedef {{ + * port?: number, + * host?: string, + * checkVersion?: boolean, + * }} NcmApiOptions + */ + +/** + * @typedef {{ + * status: VERSION_CHECK_RESULT, + * ourVersion?: string, + * npmVersion?: string, + * }} VersionCheckResult + */ + +/** + * @typedef {{ + * server?: import('http').Server, + * }} ExpressExtension + */ + +/** + * Check if the version of this API is latest. + * + * @returns {Promise} 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, reject) => { + exec('npm info NeteaseCloudMusicApi version', (err, stdout, stderr) => { + 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. + * + * @returns {Promise} The server instance. + */ +async function consturctServer() { + 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+|(? { + 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(bodyParser.json()) + app.use(bodyParser.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 modules = ( + await fs.promises.readdir(path.join(__dirname, 'module')) + ).reverse() + for (const file of modules) { + // Check if the file is written in JS. + if (!file.endsWith('.js')) continue + + // Get the route path. + const route = + file in special + ? special[file] + : '/' + file.replace(/\.js$/i, '').replace(/_/g, '/') + + // Get the module itself. + const module = require(path.join(__dirname, 'module', file)) + + // Register the route. + app.use(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 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} + */ +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() + + 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 From 0ac00325c8e87396b409498f33ce542b120bb833 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 11:34:35 +0800 Subject: [PATCH 02/10] refactor(server): replace the deprecated function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Express 4.x 以上版本已經內建 body-parser。 --- server.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index 4c52653fc34..87dbed26d43 100644 --- a/server.js +++ b/server.js @@ -2,7 +2,6 @@ const fs = require('fs') const path = require('path') 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 @@ -125,8 +124,8 @@ async function consturctServer() { /** * Body Parser and File Upload */ - app.use(bodyParser.json()) - app.use(bodyParser.urlencoded({ extended: false })) + app.use(express.json()) + app.use(express.urlencoded({ extended: false })) app.use(fileUpload()) From fb04e512c034ea323ab0e2f3b1effc68429bb6a8 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 11:38:08 +0800 Subject: [PATCH 03/10] fix(server): remove the shebang line --- server.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server.js b/server.js index 87dbed26d43..483b04ca9b0 100644 --- a/server.js +++ b/server.js @@ -1,4 +1,3 @@ -#!/usr/bin/env node const fs = require('fs') const path = require('path') const express = require('express') From fc9630aa39bfbf2b382688a5d8f436579e09bd8d Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 11:52:46 +0800 Subject: [PATCH 04/10] refactor: add type definition of Express --- package-lock.json | 180 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + 2 files changed, 182 insertions(+) diff --git a/package-lock.json b/package-lock.json index 227a6eec637..e938d802ba9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,8 @@ "NeteaseCloudMusicApi": "app.js" }, "devDependencies": { + "@types/express": "^4.17.13", + "@types/express-fileupload": "^1.2.2", "@types/node": "16.11.19", "@typescript-eslint/eslint-plugin": "5.0.0", "@typescript-eslint/parser": "5.0.0", @@ -186,22 +188,101 @@ "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": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", "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": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "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/node": { "version": "16.11.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz", "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": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.9.tgz", @@ -211,6 +292,16 @@ "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": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.0.0.tgz", @@ -5772,22 +5863,101 @@ "resolved": "https://registry.npm.taobao.org/@tootallnate/once/download/@tootallnate/once-1.1.2.tgz", "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": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", "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": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "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/node": { "version": "16.11.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz", "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": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.9.tgz", @@ -5797,6 +5967,16 @@ "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": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.0.0.tgz", diff --git a/package.json b/package.json index bd88d0bb157..a403c13e093 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "yargs": "^17.1.1" }, "devDependencies": { + "@types/express": "^4.17.13", + "@types/express-fileupload": "^1.2.2", "@types/node": "16.11.19", "@typescript-eslint/eslint-plugin": "5.0.0", "@typescript-eslint/parser": "5.0.0", From 5bf53028ef47e7f686c975d485234b658e8712c7 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 14:14:24 +0800 Subject: [PATCH 05/10] feat(server): allow passing modules manually MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自行維護 require 名單以相容 Webpack。 --- app.js | 2 +- server.js | 76 ++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/app.js b/app.js index 3e5e976d2d0..de14190feef 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,4 @@ #!/usr/bin/env node -require('./server')({ +require('./server').serveNcmApi({ checkVersion: true, }) diff --git a/server.js b/server.js index 483b04ca9b0..60585f146ae 100644 --- a/server.js +++ b/server.js @@ -20,11 +20,19 @@ const VERSION_CHECK_RESULT = { LATEST: 1, } +/** + * @typedef {{ + * route: string, + * module: any + * }} ModuleDefinition + */ + /** * @typedef {{ * port?: number, * host?: string, * checkVersion?: boolean, + * moduleDefs?: ModuleDefinition[] * }} NcmApiOptions */ @@ -42,6 +50,35 @@ const VERSION_CHECK_RESULT = { * }} ExpressExtension */ +/** + * Get the module definitions dynamically. + * + * @param {string} modulePath The path to modules (JS). + * @param {Record} [specificRoute] The specific route of specific modules. + * @returns {Promise} The module definitions. + * + * @example getModuleDefinitions("./module", {"album_new.js": "/album/create"}) + */ +async function getModulesDefinitions(modulePath, specificRoute) { + const files = await fs.promises.readdir(path.join(__dirname, 'module')) + 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 route = parseRoute(file) + const module = require(path.join(modulePath, file)) + + return { route, module } + }) + + return modules +} + /** * Check if the version of this API is latest. * @@ -50,8 +87,8 @@ const VERSION_CHECK_RESULT = { * need to notify users to upgrade it manually. */ async function checkVersion() { - return new Promise((resolve, reject) => { - exec('npm info NeteaseCloudMusicApi version', (err, stdout, stderr) => { + return new Promise((resolve) => { + exec('npm info NeteaseCloudMusicApi version', (err, stdout) => { if (!err) { let version = stdout.trim() @@ -82,9 +119,10 @@ async function checkVersion() { /** * Construct the server of NCM API. * + * @param {ModuleDefinition[]} moduleDefs Customized module definitions [advanced] * @returns {Promise} The server instance. */ -async function consturctServer() { +async function consturctServer(moduleDefs) { const app = express() app.set('trust proxy', true) @@ -107,7 +145,7 @@ async function consturctServer() { /** * Cookie Parser */ - app.use((req, res, next) => { + app.use((req, _, next) => { req.cookies = {} //;(req.headers.cookie || '').split(/\s*;\s*/).forEach((pair) => { // Polynomial regular expression // ;(req.headers.cookie || '').split(/;\s+|(? { @@ -150,24 +188,13 @@ async function consturctServer() { /** * Load every modules in this directory */ - const modules = ( - await fs.promises.readdir(path.join(__dirname, 'module')) - ).reverse() - for (const file of modules) { - // Check if the file is written in JS. - if (!file.endsWith('.js')) continue - - // Get the route path. - const route = - file in special - ? special[file] - : '/' + file.replace(/\.js$/i, '').replace(/_/g, '/') - - // Get the module itself. - const module = require(path.join(__dirname, 'module', file)) + const moduleDefinitions = + moduleDefs || + (await getModulesDefinitions(path.join(__dirname, 'module'), special)) + for (const moduleDef of moduleDefinitions) { // Register the route. - app.use(route, async (req, res) => { + app.use(moduleDef.route, async (req, res) => { ;[req.query, req.body].forEach((item) => { if (typeof item.cookie === 'string') { item.cookie = cookieToJson(decode(item.cookie)) @@ -182,7 +209,7 @@ async function consturctServer() { ) try { - const moduleResponse = await module(query, request) + const moduleResponse = await moduleDef.module(query, request) console.log('[OK]', decode(req.originalUrl)) const cookies = moduleResponse.cookie @@ -242,7 +269,7 @@ async function serveNcmApi(options) { ) } }) - const constructServerSubmission = consturctServer() + const constructServerSubmission = consturctServer(options.moduleDefs) const [_, app] = await Promise.all([ checkVersionSubmission, @@ -258,4 +285,7 @@ async function serveNcmApi(options) { return appExt } -module.exports = serveNcmApi +module.exports = { + serveNcmApi, + getModulesDefinitions, +} From 4e434a26917e82cfc92f24f7f9269c1f8a46c413 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 14:20:54 +0800 Subject: [PATCH 06/10] feat(server): add field "identifier" I'm going to replace the function of "main.js". --- server.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index 60585f146ae..ff63c092a85 100644 --- a/server.js +++ b/server.js @@ -22,6 +22,7 @@ const VERSION_CHECK_RESULT = { /** * @typedef {{ + * identifier?: string, * route: string, * module: any * }} ModuleDefinition @@ -70,10 +71,11 @@ async function getModulesDefinitions(modulePath, specificRoute) { .reverse() .filter((file) => file.endsWith('.js')) .map((file) => { + const identifier = file.split('.').shift() const route = parseRoute(file) const module = require(path.join(modulePath, file)) - return { route, module } + return { identifier, route, module } }) return modules From 9e0c900f6fb1a9be9e442b848d8304cf75c89ed4 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 14:30:00 +0800 Subject: [PATCH 07/10] test(server): more reliable test for server --- app.test.js | 15 --------------- package-lock.json | 13 +++++++++++++ package.json | 3 ++- server.test.js | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 16 deletions(-) delete mode 100644 app.test.js create mode 100644 server.test.js diff --git a/app.test.js b/app.test.js deleted file mode 100644 index 94846b28250..00000000000 --- a/app.test.js +++ /dev/null @@ -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)) -}) diff --git a/package-lock.json b/package-lock.json index e938d802ba9..b90d27d045b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "devDependencies": { "@types/express": "^4.17.13", "@types/express-fileupload": "^1.2.2", + "@types/mocha": "^9.1.0", "@types/node": "16.11.19", "@typescript-eslint/eslint-plugin": "5.0.0", "@typescript-eslint/parser": "5.0.0", @@ -266,6 +267,12 @@ "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": { "version": "16.11.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz", @@ -5941,6 +5948,12 @@ "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": { "version": "16.11.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz", diff --git a/package.json b/package.json index a403c13e093..c40bf24ef03 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "网易云音乐 NodeJS 版 API", "scripts": { "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-fix": "eslint --fix \"**/*.{js,ts}\"", "prepare": "husky install", @@ -58,6 +58,7 @@ "devDependencies": { "@types/express": "^4.17.13", "@types/express-fileupload": "^1.2.2", + "@types/mocha": "^9.1.0", "@types/node": "16.11.19", "@typescript-eslint/eslint-plugin": "5.0.0", "@typescript-eslint/parser": "5.0.0", diff --git a/server.test.js b/server.test.js new file mode 100644 index 00000000000..e3d28b27fe7 --- /dev/null +++ b/server.test.js @@ -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)) +}) From 7061a9ea82651d99081f683d3f5da1b4a9bd24b7 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 14:34:35 +0800 Subject: [PATCH 08/10] refactor!: use server.js as main.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit server.js 有 main.js 原本的功能 (getModulesDefinitions) BREAKING CHANGES: 所有使用到這個 lib 的應用程式 ,皆需更改為 .getModulesDefinitions()。 --- main.js | 26 -------------------------- package.json | 2 +- server.js | 2 +- 3 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 main.js diff --git a/main.js b/main.js deleted file mode 100644 index 0b214902ce3..00000000000 --- a/main.js +++ /dev/null @@ -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 diff --git a/package.json b/package.json index c40bf24ef03..0fde53f9d6d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "音乐", "网易云音乐nodejs" ], - "main": "main.js", + "main": "server.js", "types": "./interface.d.ts", "engines": { "node": ">=12" diff --git a/server.js b/server.js index ff63c092a85..54c03cc5ad6 100644 --- a/server.js +++ b/server.js @@ -121,7 +121,7 @@ async function checkVersion() { /** * Construct the server of NCM API. * - * @param {ModuleDefinition[]} moduleDefs Customized module definitions [advanced] + * @param {ModuleDefinition[]} [moduleDefs] Customized module definitions [advanced] * @returns {Promise} The server instance. */ async function consturctServer(moduleDefs) { From c412f53f115b5b5259d903a07f04ebfe8319c744 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 14:42:50 +0800 Subject: [PATCH 09/10] feat(server): allow printing out module paths --- server.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 54c03cc5ad6..4d8e4b086bd 100644 --- a/server.js +++ b/server.js @@ -54,14 +54,20 @@ const VERSION_CHECK_RESULT = { /** * Get the module definitions dynamically. * - * @param {string} modulePath The path to modules (JS). + * @param {string} modulesPath The path to modules (JS). * @param {Record} [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} The module definitions. * * @example getModuleDefinitions("./module", {"album_new.js": "/album/create"}) */ -async function getModulesDefinitions(modulePath, specificRoute) { - const files = await fs.promises.readdir(path.join(__dirname, 'module')) +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] @@ -73,7 +79,8 @@ async function getModulesDefinitions(modulePath, specificRoute) { .map((file) => { const identifier = file.split('.').shift() const route = parseRoute(file) - const module = require(path.join(modulePath, file)) + const modulePath = path.join(modulesPath, file) + const module = doRequire ? require(modulePath) : modulePath return { identifier, route, module } }) From 69a71cb71093b9013201df4bda51452b72a79564 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 26 Jan 2022 14:50:50 +0800 Subject: [PATCH 10/10] chore: add a tool to get the static module definitions --- .gitignore | 1 + examples/get_static_moddef.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 examples/get_static_moddef.js diff --git a/.gitignore b/.gitignore index c9735e7af15..4bbcc787ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules .idea .vscode .history +examples/moddef.json diff --git a/examples/get_static_moddef.js b/examples/get_static_moddef.js new file mode 100644 index 00000000000..f430aeddfed --- /dev/null +++ b/examples/get_static_moddef.js @@ -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()