diff --git a/generateConfig.js b/generateConfig.js index 7640065..6168586 100644 --- a/generateConfig.js +++ b/generateConfig.js @@ -4,38 +4,167 @@ const { register_anonimous } = require('./main') const { cookieToJson, generateRandomChineseIP } = require('./util/index') const { getXeapiPublicKey } = require('./util/xeapiKey') const tmpPath = require('os').tmpdir() +const logger = require('./util/logger') -async function generateConfig() { - global.cnIp = generateRandomChineseIP() - try { - const res = await register_anonimous() - const cookie = res.body.cookie - if (cookie) { - const cookieObj = cookieToJson(cookie) +const MAX_RETRIES = 3 +const RETRY_DELAY_MS = 1000 + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +function isRetryableError(err) { + const msg = (err && err.message) || '' + const status = + (err && err.status) || (err && err.response && err.response.status) + if ( + msg.includes('ETIMEDOUT') || + msg.includes('ECONNRESET') || + msg.includes('ECONNREFUSED') || + msg.includes('socket hang up') || + msg.includes('request timeout') || + msg.includes('timeout') || + msg.includes('network') || + msg.includes('Network') + ) { + return true + } + if (status && status >= 500) { + return true + } + return false +} + +/** @returns {{ success: boolean, error?: Error }} */ +async function fetchAnonymousToken() { + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + const res = await register_anonimous() + const cookie = res.body.cookie + if (cookie) { + const cookieObj = cookieToJson(cookie) + fs.writeFileSync( + path.resolve(tmpPath, 'anonymous_token'), + cookieObj.MUSIC_A, + 'utf-8', + ) + logger.success('[generateConfig] 匿名 token 注册成功') + return { success: true } + } + // 返回了但没有 cookie,视为异常但不再重试 + logger.warn( + `[generateConfig] 匿名注册返回了空 cookie (attempt ${attempt})`, + ) + return { + success: false, + error: new Error('empty cookie from anonymous register'), + } + } catch (err) { + if (isRetryableError(err) && attempt < MAX_RETRIES) { + const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1) + logger.warn( + `[generateConfig] 获取匿名 token 失败 (attempt ${attempt}/${MAX_RETRIES}), ${delay}ms 后重试...`, + ) + await sleep(delay) + continue + } + // 不可重试 或 已达最大次数 + if (attempt >= MAX_RETRIES) { + logger.error( + `[generateConfig] 获取匿名 token 已达最大重试次数 (${MAX_RETRIES}):`, + err.message, + ) + } else { + logger.error( + `[generateConfig] 获取匿名 token 失败 (不可重试):`, + err.message, + ) + } + return { success: false, error: err } + } + } + return { success: false, error: new Error('unreachable') } +} + +/** + * 获取 xeapi public key,带重试 + * @returns {{ success: boolean, error?: Error }} + */ +async function fetchXeapiPublicKey() { + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + let currentPublicKey = {} + try { + currentPublicKey = JSON.parse( + fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'), + ) + } catch (_) { + // 本地无缓存文件,用空对象正常请求 + } + const publicKey = await getXeapiPublicKey( + currentPublicKey, + global.deviceId, + ) fs.writeFileSync( - path.resolve(tmpPath, 'anonymous_token'), - cookieObj.MUSIC_A, + path.resolve(tmpPath, 'xeapi_public_key'), + JSON.stringify(publicKey), 'utf-8', ) + logger.success('[generateConfig] xeapi public key 获取成功') + return { success: true } + } catch (err) { + if (isRetryableError(err) && attempt < MAX_RETRIES) { + const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1) + logger.warn( + `[generateConfig] 获取 xeapi public key 失败 (attempt ${attempt}/${MAX_RETRIES}), ${delay}ms 后重试...`, + ) + await sleep(delay) + continue + } + if (attempt >= MAX_RETRIES) { + logger.error( + `[generateConfig] 获取 xeapi public key 已达最大重试次数 (${MAX_RETRIES}):`, + err.message, + ) + } else { + logger.error( + `[generateConfig] 获取 xeapi public key 失败 (不可重试):`, + err.message, + ) + } + return { success: false, error: err } } - } catch (error) { - console.log(error) } - try { - let currentPublicKey = {} - try { - currentPublicKey = JSON.parse( - fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'), - ) - } catch (_) {} - const publicKey = await getXeapiPublicKey(currentPublicKey, global.deviceId) - fs.writeFileSync( - path.resolve(tmpPath, 'xeapi_public_key'), - JSON.stringify(publicKey), - 'utf-8', - ) - } catch (error) { - console.log(error) + return { success: false, error: new Error('unreachable') } +} + +/** + * 生成配置(匿名 token + xeapi public key),带容错重试 + * @returns {{ tokenOk: boolean, keyOk: boolean }} + */ +async function generateConfig() { + global.cnIp = generateRandomChineseIP() + + // 两个任务并行执行,互不影响喵~ + const [tokenResult, keyResult] = await Promise.all([ + fetchAnonymousToken(), + fetchXeapiPublicKey(), + ]) + + if (!tokenResult.success) { + logger.warn('[generateConfig] 匿名 token 获取失败') + } + if (!keyResult.success) { + logger.warn('[generateConfig] xeapi public key 获取失败') + } + + if (tokenResult.success && keyResult.success) { + logger.success('[generateConfig] 配置初始化完成') + } + + return { + tokenOk: tokenResult.success, + keyOk: keyResult.success, } } module.exports = generateConfig diff --git a/module/register_anonimous.js b/module/register_anonimous.js index ae76ad9..a60798b 100644 --- a/module/register_anonimous.js +++ b/module/register_anonimous.js @@ -26,7 +26,6 @@ function cloudmusic_dll_encode_id(some_id) { module.exports = async (query, request) => { const deviceId = generateDeviceId() - logger.info(`Successfully registered anonimous token, deviceId: ${deviceId}`) global.deviceId = deviceId const encodedId = CryptoJS.enc.Base64.stringify( CryptoJS.enc.Utf8.parse( diff --git a/server.js b/server.js index 06d7055..45dce9a 100644 --- a/server.js +++ b/server.js @@ -10,6 +10,7 @@ const { cookieToJson } = require('./util/index') const fileUpload = require('express-fileupload') const decode = require('safe-decode-uri-component') const logger = require('./util/logger.js') +const { APP_CONF } = require('./util/config.json') /** * The version check result. @@ -299,15 +300,15 @@ async function constructServer(moduleDefs) { ) try { + let usedCrypto = '' const moduleResponse = await moduleDef.module(query, (...params) => { - // 参数注入客户端IP const obj = [...params] const options = obj[2] || {} + usedCrypto = options.crypto || '' let ip = '' if (options.randomCNIP) { ip = global.cnIp - // logger.info('Using random Chinese IP for request:', ip) } else { ip = req.ip @@ -317,7 +318,6 @@ async function constructServer(moduleDefs) { if (ip == '::1') { ip = global.cnIp } - // logger.info('Requested from ip:', ip) } obj[2] = { @@ -327,7 +327,10 @@ async function constructServer(moduleDefs) { return request(...obj) }) - logger.info(`Request Success: ${decode(req.originalUrl)}`) + const displayCrypto = usedCrypto || (APP_CONF.encrypt ? 'eapi' : 'api') + logger.info( + `Request Success: [${displayCrypto}] ${decode(req.originalUrl)}`, + ) // 夹带私货部分:如果开启了通用解锁,并且是获取歌曲URL的接口,则尝试解锁(如果需要的话)ヾ(≧▽≦*)o if ( @@ -445,10 +448,7 @@ async function serveNcmApi(options) { ╩ ╩╩ ╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝ `) logger.info(` -- Server started successfully @ http://${host ? host : 'localhost'}:${port} -- Environment: ${process.env.NODE_ENV || 'development'} -- Node Version: ${process.version} -- Process ID: ${process.pid}`) +- Server started successfully @ http://${host ? host : 'localhost'}:${port}`) }) return appExt