From 4489f10f637ed1bad5ed95917b236906edf0f210 Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:08:40 +0800 Subject: [PATCH 01/16] =?UTF-8?q?feat(server):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=A4=A7=E5=B0=8F=E9=99=90?= =?UTF-8?q?=E5=88=B6=E5=B9=B6=E4=BC=98=E5=8C=96=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将请求体大小限制从 50MB 增加到 500MB - 配置文件上传中间件支持 500MB 大小限制 - 设置临时文件目录为 /tmp/ - 启用 useTempFiles 选项以提高性能 - 配置达到限制时自动中止上传 --- server.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index a9f53db..3736761 100644 --- a/server.js +++ b/server.js @@ -178,10 +178,17 @@ async function consturctServer(moduleDefs) { /** * Body Parser and File Upload */ - app.use(express.json({ limit: '50mb' })) - app.use(express.urlencoded({ extended: false, limit: '50mb' })) + app.use(express.json({ limit: '500mb' })) + app.use(express.urlencoded({ extended: false, limit: '500mb' })) - app.use(fileUpload()) + app.use(fileUpload({ + limits: { + fileSize: 500 * 1024 * 1024 // 500MB + }, + useTempFiles: false, + tempFileDir: '/tmp/', + abortOnLimit: true + })) /** * Cache From 26d55255e091d65cdf98bdc347c1828e0b07780f Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:17:07 +0800 Subject: [PATCH 02/16] =?UTF-8?q?fix(cloud):=20=E8=A7=A3=E5=86=B3=E4=BA=91?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E4=B8=AD=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C=E4=BB=A3=E7=A0=81=E6=B8=85?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除注释掉的废弃代码和调试信息 - 添加详细的错误处理和日志记录 - 验证token分配结果并处理失败情况 - 在上传过程中添加异常捕获和错误抛出 - 验证LBS响应的有效性并处理网络请求超时 - 改进上传流程的错误处理机制 --- module/cloud.js | 75 ++++++++++++++++++++++--------------------- plugins/songUpload.js | 63 +++++++++++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 45 deletions(-) diff --git a/module/cloud.js b/module/cloud.js index 978b7f7..4fd6172 100644 --- a/module/cloud.js +++ b/module/cloud.js @@ -6,9 +6,6 @@ let mm module.exports = async (query, request) => { mm = require('music-metadata') let ext = 'mp3' - // if (query.songFile.name.indexOf('flac') > -1) { - // ext = 'flac' - // } if (query.songFile.name.includes('.')) { ext = query.songFile.name.split('.').pop() } @@ -30,7 +27,6 @@ module.exports = async (query, request) => { }) } if (!query.songFile.md5) { - // 命令行上传没有md5和size信息,需要填充 query.songFile.md5 = md5(query.songFile.data) query.songFile.size = query.songFile.data.byteLength } @@ -65,32 +61,8 @@ module.exports = async (query, request) => { if (info.artist) { artist = info.artist } - // if (metadata.native.ID3v1) { - // metadata.native.ID3v1.forEach((item) => { - // // logger.info(item.id, item.value) - // if (item.id === 'title') { - // songName = item.value - // } - // if (item.id === 'artist') { - // artist = item.value - // } - // if (item.id === 'album') { - // album = item.value - // } - // }) - // // logger.info({ - // // songName, - // // album, - // // songName, - // // }) - // } - // logger.info({ - // songName, - // album, - // songName, - // }) } catch (error) { - logger.info(error) + logger.info('metadata parse error:', error.message) } const tokenRes = await request( `/api/nos/token/alloc`, @@ -106,11 +78,31 @@ module.exports = async (query, request) => { createOption(query), ) - if (res.body.needUpload) { - const uploadInfo = await uploadPlugin(query, request) - // logger.info('uploadInfo', uploadInfo.body.result.resourceId) + if (!tokenRes.body.result || !tokenRes.body.result.resourceId) { + logger.error('Token allocation failed:', tokenRes.body) + return Promise.reject({ + status: 500, + body: { + code: 500, + msg: '获取上传token失败', + detail: tokenRes.body, + }, + }) } - // logger.info(tokenRes.body.result) + + if (res.body.needUpload) { + logger.info('Need upload, starting upload process...') + try { + const uploadInfo = await uploadPlugin(query, request) + logger.info('Upload completed:', uploadInfo?.body?.result?.resourceId) + } catch (uploadError) { + logger.error('Upload failed:', uploadError) + return Promise.reject(uploadError) + } + } else { + logger.info('File already exists, skip upload') + } + const res2 = await request( `/api/upload/cloud/info/v2`, { @@ -125,8 +117,19 @@ module.exports = async (query, request) => { }, createOption(query), ) - // logger.info({ res2, privateCloud: res2.body.privateCloud }) - // logger.info(res.body.songId, 'songid') + + if (res2.body.code !== 200 && res2.body.code !== 200) { + logger.error('Cloud info upload failed:', res2.body) + return Promise.reject({ + status: res2.status || 500, + body: { + code: res2.body.code || 500, + msg: res2.body.msg || '上传云盘信息失败', + detail: res2.body, + }, + }) + } + const res3 = await request( `/api/cloud/pub/v2`, { @@ -134,13 +137,11 @@ module.exports = async (query, request) => { }, createOption(query), ) - // logger.info({ res3 }) return { status: 200, body: { ...res.body, ...res3.body, - // ...uploadInfo, }, cookie: res.cookie, } diff --git a/plugins/songUpload.js b/plugins/songUpload.js index 0965bd8..09c7745 100644 --- a/plugins/songUpload.js +++ b/plugins/songUpload.js @@ -3,9 +3,6 @@ const createOption = require('../util/option.js') const logger = require('../util/logger.js') module.exports = async (query, request) => { let ext = 'mp3' - // if (query.songFile.name.indexOf('flac') > -1) { - // ext = 'flac' - // } if (query.songFile.name.includes('.')) { ext = query.songFile.name.split('.').pop() } @@ -14,7 +11,6 @@ module.exports = async (query, request) => { .replace(/\s/g, '') .replace(/\./g, '_') const bucket = 'jd-musicrep-privatecloud-audio-public' - // 获取key和token const tokenRes = await request( `/api/nos/token/alloc`, { @@ -29,15 +25,53 @@ module.exports = async (query, request) => { createOption(query, 'weapi'), ) - // 上传 + if (!tokenRes.body.result || !tokenRes.body.result.objectKey) { + logger.error('Token allocation failed:', tokenRes.body) + throw { + status: 500, + body: { + code: 500, + msg: '获取上传token失败', + detail: tokenRes.body, + }, + } + } + const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F') + let lbs try { - const lbs = ( + lbs = ( await axios({ method: 'get', url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`, + timeout: 10000, }) ).data + } catch (error) { + logger.error('LBS fetch failed:', error.message) + throw { + status: 500, + body: { + code: 500, + msg: '获取上传服务器地址失败', + detail: error.message, + }, + } + } + + if (!lbs || !lbs.upload || !lbs.upload[0]) { + logger.error('Invalid LBS response:', lbs) + throw { + status: 500, + body: { + code: 500, + msg: '获取上传服务器地址无效', + detail: lbs, + }, + } + } + + try { await axios({ method: 'post', url: `${lbs.upload[0]}/${bucket}/${objectKey}?offset=0&complete=true&version=1.0`, @@ -50,10 +84,23 @@ module.exports = async (query, request) => { data: query.songFile.data, maxContentLength: Infinity, maxBodyLength: Infinity, + timeout: 300000, }) + logger.info('Upload success:', filename) } catch (error) { - logger.info('error', error.response) - throw error.response + logger.error('Upload failed:', { + status: error.response?.status, + data: error.response?.data, + message: error.message, + }) + throw { + status: error.response?.status || 500, + body: { + code: error.response?.status || 500, + msg: '文件上传失败', + detail: error.response?.data || error.message, + }, + } } return { ...tokenRes, From ba7d1a8574e56a437aa1dc43dbe1ed001e578317 Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:44:19 +0800 Subject: [PATCH 03/16] =?UTF-8?q?feat(cloud):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 crypto 模块替换 md5 模块进行文件哈希计算 - 添加对临时文件上传的支持,当存在 tempFilePath 时使用文件流处理 - 实现临时文件的 MD5 计算和元数据解析功能 - 在上传完成后自动清理临时文件 - 配置服务器端文件上传中间件启用临时文件支持 - 修改上传插件以支持临时文件读取流上传方式 - 增加文件大小获取和验证的兼容性处理 --- module/cloud.js | 60 ++++++++++++++++++++++++++++++++++--------- plugins/songUpload.js | 11 +++++++- server.js | 9 ++++--- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/module/cloud.js b/module/cloud.js index 4fd6172..0b79917 100644 --- a/module/cloud.js +++ b/module/cloud.js @@ -1,5 +1,6 @@ const uploadPlugin = require('../plugins/songUpload') -const md5 = require('md5') +const crypto = require('crypto') +const fs = require('fs') const createOption = require('../util/option.js') const logger = require('../util/logger.js') let mm @@ -26,17 +27,40 @@ module.exports = async (query, request) => { }, }) } - if (!query.songFile.md5) { - query.songFile.md5 = md5(query.songFile.data) - query.songFile.size = query.songFile.data.byteLength + + const useTempFile = !!query.songFile.tempFilePath + let fileSize = query.songFile.size + let fileMd5 = query.songFile.md5 + + if (useTempFile) { + const stats = fs.statSync(query.songFile.tempFilePath) + fileSize = stats.size + if (!fileMd5) { + fileMd5 = await new Promise((resolve, reject) => { + const hash = crypto.createHash('md5') + const stream = fs.createReadStream(query.songFile.tempFilePath) + stream.on('data', (chunk) => hash.update(chunk)) + stream.on('end', () => resolve(hash.digest('hex'))) + stream.on('error', reject) + }) + } + } else { + if (!fileMd5) { + fileMd5 = crypto.createHash('md5').update(query.songFile.data).digest('hex') + } + fileSize = query.songFile.data.byteLength } + + query.songFile.md5 = fileMd5 + query.songFile.size = fileSize + const res = await request( `/api/cloud/upload/check`, { bitrate: String(bitrate), ext: '', - length: query.songFile.size, - md5: query.songFile.md5, + length: fileSize, + md5: fileMd5, songId: '0', version: 1, }, @@ -46,10 +70,15 @@ module.exports = async (query, request) => { let album = '' let songName = '' try { - const metadata = await mm.parseBuffer( - query.songFile.data, - query.songFile.mimetype, - ) + let metadata + if (useTempFile) { + metadata = await mm.parseFile(query.songFile.tempFilePath) + } else { + metadata = await mm.parseBuffer( + query.songFile.data, + query.songFile.mimetype, + ) + } const info = metadata.common if (info.title) { @@ -73,7 +102,7 @@ module.exports = async (query, request) => { local: false, nos_product: 3, type: 'audio', - md5: query.songFile.md5, + md5: fileMd5, }, createOption(query), ) @@ -98,15 +127,22 @@ module.exports = async (query, request) => { } catch (uploadError) { logger.error('Upload failed:', uploadError) return Promise.reject(uploadError) + } finally { + if (useTempFile && fs.existsSync(query.songFile.tempFilePath)) { + fs.unlinkSync(query.songFile.tempFilePath) + } } } else { logger.info('File already exists, skip upload') + if (useTempFile && fs.existsSync(query.songFile.tempFilePath)) { + fs.unlinkSync(query.songFile.tempFilePath) + } } const res2 = await request( `/api/upload/cloud/info/v2`, { - md5: query.songFile.md5, + md5: fileMd5, songid: res.body.songId, filename: query.songFile.name, song: songName || filename, diff --git a/plugins/songUpload.js b/plugins/songUpload.js index 09c7745..642d863 100644 --- a/plugins/songUpload.js +++ b/plugins/songUpload.js @@ -1,4 +1,5 @@ const { default: axios } = require('axios') +const fs = require('fs') const createOption = require('../util/option.js') const logger = require('../util/logger.js') module.exports = async (query, request) => { @@ -71,6 +72,14 @@ module.exports = async (query, request) => { } } + const useTempFile = !!query.songFile.tempFilePath + let uploadData + if (useTempFile) { + uploadData = fs.createReadStream(query.songFile.tempFilePath) + } else { + uploadData = query.songFile.data + } + try { await axios({ method: 'post', @@ -81,7 +90,7 @@ module.exports = async (query, request) => { 'Content-Type': 'audio/mpeg', 'Content-Length': String(query.songFile.size), }, - data: query.songFile.data, + data: uploadData, maxContentLength: Infinity, maxBodyLength: Infinity, timeout: 300000, diff --git a/server.js b/server.js index 3736761..a02d4d8 100644 --- a/server.js +++ b/server.js @@ -183,11 +183,12 @@ async function consturctServer(moduleDefs) { app.use(fileUpload({ limits: { - fileSize: 500 * 1024 * 1024 // 500MB + fileSize: 500 * 1024 * 1024 }, - useTempFiles: false, - tempFileDir: '/tmp/', - abortOnLimit: true + useTempFiles: true, + tempFileDir: require('os').tmpdir(), + abortOnLimit: true, + parseNested: true })) /** From 2d6173b2aaabf0fe3543ce73173213026c23dac0 Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:50:20 +0800 Subject: [PATCH 04/16] =?UTF-8?q?fix(cloud):=20=E7=A7=BB=E9=99=A4=E4=BA=86?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E7=9A=84=E6=9D=A1=E4=BB=B6=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/cloud.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/cloud.js b/module/cloud.js index 0b79917..3b8913e 100644 --- a/module/cloud.js +++ b/module/cloud.js @@ -154,7 +154,7 @@ module.exports = async (query, request) => { createOption(query), ) - if (res2.body.code !== 200 && res2.body.code !== 200) { + if (res2.body.code !== 200) { logger.error('Cloud info upload failed:', res2.body) return Promise.reject({ status: res2.status || 500, From 33ccc83615f5c74730adbadfff573dd100a8f86e Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:57:20 +0800 Subject: [PATCH 05/16] =?UTF-8?q?fix(cloud):=20=E8=A7=A3=E5=86=B3=E4=B8=B4?= =?UTF-8?q?=E6=97=B6=E6=96=87=E4=BB=B6=E6=B8=85=E7=90=86=E5=92=8C=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=93=8D=E4=BD=9C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加异步临时文件清理函数避免资源泄露 - 将同步文件操作改为异步操作提高性能 - 在令牌分配失败时执行临时文件清理 - 在上传失败时确保临时文件被清理 - 使用 Promise 包装 MD5 计算操作 - 统一临时文件清理逻辑到 finally 块 --- module/cloud.js | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/module/cloud.js b/module/cloud.js index 3b8913e..2f80fc8 100644 --- a/module/cloud.js +++ b/module/cloud.js @@ -32,9 +32,32 @@ module.exports = async (query, request) => { let fileSize = query.songFile.size let fileMd5 = query.songFile.md5 + const cleanupTempFile = async () => { + if (useTempFile) { + try { + await fs.promises.unlink(query.songFile.tempFilePath) + } catch (e) { + logger.info('Temp file cleanup failed:', e.message) + } + } + } + if (useTempFile) { - const stats = fs.statSync(query.songFile.tempFilePath) - fileSize = stats.size + try { + const stats = await fs.promises.stat(query.songFile.tempFilePath) + fileSize = stats.size + } catch (e) { + logger.error('Failed to stat temp file:', e.message) + await cleanupTempFile() + return Promise.reject({ + status: 500, + body: { + code: 500, + msg: '临时文件访问失败', + detail: e.message, + }, + }) + } if (!fileMd5) { fileMd5 = await new Promise((resolve, reject) => { const hash = crypto.createHash('md5') @@ -46,7 +69,11 @@ module.exports = async (query, request) => { } } else { if (!fileMd5) { - fileMd5 = crypto.createHash('md5').update(query.songFile.data).digest('hex') + fileMd5 = await new Promise((resolve) => { + setImmediate(() => { + resolve(crypto.createHash('md5').update(query.songFile.data).digest('hex')) + }) + }) } fileSize = query.songFile.data.byteLength } @@ -109,6 +136,7 @@ module.exports = async (query, request) => { if (!tokenRes.body.result || !tokenRes.body.result.resourceId) { logger.error('Token allocation failed:', tokenRes.body) + await cleanupTempFile() return Promise.reject({ status: 500, body: { @@ -126,19 +154,15 @@ module.exports = async (query, request) => { logger.info('Upload completed:', uploadInfo?.body?.result?.resourceId) } catch (uploadError) { logger.error('Upload failed:', uploadError) + await cleanupTempFile() return Promise.reject(uploadError) - } finally { - if (useTempFile && fs.existsSync(query.songFile.tempFilePath)) { - fs.unlinkSync(query.songFile.tempFilePath) - } } } else { logger.info('File already exists, skip upload') - if (useTempFile && fs.existsSync(query.songFile.tempFilePath)) { - fs.unlinkSync(query.songFile.tempFilePath) - } } + await cleanupTempFile() + const res2 = await request( `/api/upload/cloud/info/v2`, { From 8951e32a0eb2fabc3e067b4f7bbff8d16ea9f89f Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:02:15 +0800 Subject: [PATCH 06/16] =?UTF-8?q?refactor(server):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E9=85=8D=E7=BD=AE=E5=92=8C?= =?UTF-8?q?MD5=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除cloud.js中的异步Promise包装,直接同步计算MD5哈希值 - 在server.js中提取上传大小限制为常量配置 - 统一使用字节单位常量管理文件上传大小限制 - 简化代码结构,提高可读性和维护性 --- module/cloud.js | 6 +----- server.js | 9 ++++++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/module/cloud.js b/module/cloud.js index 2f80fc8..bb5f157 100644 --- a/module/cloud.js +++ b/module/cloud.js @@ -69,11 +69,7 @@ module.exports = async (query, request) => { } } else { if (!fileMd5) { - fileMd5 = await new Promise((resolve) => { - setImmediate(() => { - resolve(crypto.createHash('md5').update(query.songFile.data).digest('hex')) - }) - }) + fileMd5 = crypto.createHash('md5').update(query.songFile.data).digest('hex') } fileSize = query.songFile.data.byteLength } diff --git a/server.js b/server.js index a02d4d8..1bc6e86 100644 --- a/server.js +++ b/server.js @@ -178,12 +178,15 @@ async function consturctServer(moduleDefs) { /** * Body Parser and File Upload */ - app.use(express.json({ limit: '500mb' })) - app.use(express.urlencoded({ extended: false, limit: '500mb' })) + const MAX_UPLOAD_SIZE_MB = 500 + const MAX_UPLOAD_SIZE_BYTES = MAX_UPLOAD_SIZE_MB * 1024 * 1024 + + app.use(express.json({ limit: `${MAX_UPLOAD_SIZE_MB}mb` })) + app.use(express.urlencoded({ extended: false, limit: `${MAX_UPLOAD_SIZE_MB}mb` })) app.use(fileUpload({ limits: { - fileSize: 500 * 1024 * 1024 + fileSize: MAX_UPLOAD_SIZE_BYTES }, useTempFiles: true, tempFileDir: require('os').tmpdir(), From 4087bafd7d1a69917e9539624049f7d1c4066dfa Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:09:02 +0800 Subject: [PATCH 07/16] =?UTF-8?q?fix(cloud):=20=E8=A7=A3=E5=86=B3=E4=B8=B4?= =?UTF-8?q?=E6=97=B6=E6=96=87=E4=BB=B6=E6=B8=85=E7=90=86=E5=92=8C=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=B5=81=E7=A8=8B=E4=B8=AD=E7=9A=84=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复了临时文件清理失败时的日志消息本地化 - 移除了重复的临时文件清理调用,统一在 finally 块中处理 - 优化了错误处理逻辑,确保在各种异常情况下正确返回错误响应 - 更新了日志消息为中文描述,提高可读性 - 修复了上传插件中的日志消息本地化问题 - 统一了错误处理流程,避免重复的清理操作 --- module/cloud.js | 291 +++++++++++++++++++++--------------------- plugins/songUpload.js | 10 +- 2 files changed, 150 insertions(+), 151 deletions(-) diff --git a/module/cloud.js b/module/cloud.js index bb5f157..6bdb579 100644 --- a/module/cloud.js +++ b/module/cloud.js @@ -37,168 +37,167 @@ module.exports = async (query, request) => { try { await fs.promises.unlink(query.songFile.tempFilePath) } catch (e) { - logger.info('Temp file cleanup failed:', e.message) + logger.info('临时文件清理失败:', e.message) } } } - if (useTempFile) { + try { + if (useTempFile) { + try { + const stats = await fs.promises.stat(query.songFile.tempFilePath) + fileSize = stats.size + } catch (e) { + logger.error('获取临时文件状态失败:', e.message) + return Promise.reject({ + status: 500, + body: { + code: 500, + msg: '临时文件访问失败', + detail: e.message, + }, + }) + } + if (!fileMd5) { + fileMd5 = await new Promise((resolve, reject) => { + const hash = crypto.createHash('md5') + const stream = fs.createReadStream(query.songFile.tempFilePath) + stream.on('data', (chunk) => hash.update(chunk)) + stream.on('end', () => resolve(hash.digest('hex'))) + stream.on('error', reject) + }) + } + } else { + if (!fileMd5) { + fileMd5 = crypto.createHash('md5').update(query.songFile.data).digest('hex') + } + fileSize = query.songFile.data.byteLength + } + + query.songFile.md5 = fileMd5 + query.songFile.size = fileSize + + const res = await request( + `/api/cloud/upload/check`, + { + bitrate: String(bitrate), + ext: '', + length: fileSize, + md5: fileMd5, + songId: '0', + version: 1, + }, + createOption(query), + ) + let artist = '' + let album = '' + let songName = '' try { - const stats = await fs.promises.stat(query.songFile.tempFilePath) - fileSize = stats.size - } catch (e) { - logger.error('Failed to stat temp file:', e.message) - await cleanupTempFile() + let metadata + if (useTempFile) { + metadata = await mm.parseFile(query.songFile.tempFilePath) + } else { + metadata = await mm.parseBuffer( + query.songFile.data, + query.songFile.mimetype, + ) + } + const info = metadata.common + + if (info.title) { + songName = info.title + } + if (info.album) { + album = info.album + } + if (info.artist) { + artist = info.artist + } + } catch (error) { + logger.info('元数据解析错误:', error.message) + } + const tokenRes = await request( + `/api/nos/token/alloc`, + { + bucket: '', + ext: ext, + filename: filename, + local: false, + nos_product: 3, + type: 'audio', + md5: fileMd5, + }, + createOption(query), + ) + + if (!tokenRes.body.result || !tokenRes.body.result.resourceId) { + logger.error('Token分配失败:', tokenRes.body) return Promise.reject({ status: 500, body: { code: 500, - msg: '临时文件访问失败', - detail: e.message, + msg: '获取上传token失败', + detail: tokenRes.body, }, }) } - if (!fileMd5) { - fileMd5 = await new Promise((resolve, reject) => { - const hash = crypto.createHash('md5') - const stream = fs.createReadStream(query.songFile.tempFilePath) - stream.on('data', (chunk) => hash.update(chunk)) - stream.on('end', () => resolve(hash.digest('hex'))) - stream.on('error', reject) + + if (res.body.needUpload) { + logger.info('需要上传,开始上传流程...') + try { + const uploadInfo = await uploadPlugin(query, request) + logger.info('上传完成:', uploadInfo?.body?.result?.resourceId) + } catch (uploadError) { + logger.error('上传失败:', uploadError) + return Promise.reject(uploadError) + } + } else { + logger.info('文件已存在,跳过上传') + } + + const res2 = await request( + `/api/upload/cloud/info/v2`, + { + md5: fileMd5, + songid: res.body.songId, + filename: query.songFile.name, + song: songName || filename, + album: album || '未知专辑', + artist: artist || '未知艺术家', + bitrate: String(bitrate), + resourceId: tokenRes.body.result.resourceId, + }, + createOption(query), + ) + + if (res2.body.code !== 200) { + logger.error('云盘信息上传失败:', res2.body) + return Promise.reject({ + status: res2.status || 500, + body: { + code: res2.body.code || 500, + msg: res2.body.msg || '上传云盘信息失败', + detail: res2.body, + }, }) } - } else { - if (!fileMd5) { - fileMd5 = crypto.createHash('md5').update(query.songFile.data).digest('hex') - } - fileSize = query.songFile.data.byteLength - } - query.songFile.md5 = fileMd5 - query.songFile.size = fileSize - - const res = await request( - `/api/cloud/upload/check`, - { - bitrate: String(bitrate), - ext: '', - length: fileSize, - md5: fileMd5, - songId: '0', - version: 1, - }, - createOption(query), - ) - let artist = '' - let album = '' - let songName = '' - try { - let metadata - if (useTempFile) { - metadata = await mm.parseFile(query.songFile.tempFilePath) - } else { - metadata = await mm.parseBuffer( - query.songFile.data, - query.songFile.mimetype, - ) + const res3 = await request( + `/api/cloud/pub/v2`, + { + songid: res2.body.songId, + }, + createOption(query), + ) + return { + status: 200, + body: { + ...res.body, + ...res3.body, + }, + cookie: res.cookie, } - const info = metadata.common - - if (info.title) { - songName = info.title - } - if (info.album) { - album = info.album - } - if (info.artist) { - artist = info.artist - } - } catch (error) { - logger.info('metadata parse error:', error.message) - } - const tokenRes = await request( - `/api/nos/token/alloc`, - { - bucket: '', - ext: ext, - filename: filename, - local: false, - nos_product: 3, - type: 'audio', - md5: fileMd5, - }, - createOption(query), - ) - - if (!tokenRes.body.result || !tokenRes.body.result.resourceId) { - logger.error('Token allocation failed:', tokenRes.body) + } finally { await cleanupTempFile() - return Promise.reject({ - status: 500, - body: { - code: 500, - msg: '获取上传token失败', - detail: tokenRes.body, - }, - }) - } - - if (res.body.needUpload) { - logger.info('Need upload, starting upload process...') - try { - const uploadInfo = await uploadPlugin(query, request) - logger.info('Upload completed:', uploadInfo?.body?.result?.resourceId) - } catch (uploadError) { - logger.error('Upload failed:', uploadError) - await cleanupTempFile() - return Promise.reject(uploadError) - } - } else { - logger.info('File already exists, skip upload') - } - - await cleanupTempFile() - - const res2 = await request( - `/api/upload/cloud/info/v2`, - { - md5: fileMd5, - songid: res.body.songId, - filename: query.songFile.name, - song: songName || filename, - album: album || '未知专辑', - artist: artist || '未知艺术家', - bitrate: String(bitrate), - resourceId: tokenRes.body.result.resourceId, - }, - createOption(query), - ) - - if (res2.body.code !== 200) { - logger.error('Cloud info upload failed:', res2.body) - return Promise.reject({ - status: res2.status || 500, - body: { - code: res2.body.code || 500, - msg: res2.body.msg || '上传云盘信息失败', - detail: res2.body, - }, - }) - } - - const res3 = await request( - `/api/cloud/pub/v2`, - { - songid: res2.body.songId, - }, - createOption(query), - ) - return { - status: 200, - body: { - ...res.body, - ...res3.body, - }, - cookie: res.cookie, } } diff --git a/plugins/songUpload.js b/plugins/songUpload.js index 642d863..13c88ac 100644 --- a/plugins/songUpload.js +++ b/plugins/songUpload.js @@ -27,7 +27,7 @@ module.exports = async (query, request) => { ) if (!tokenRes.body.result || !tokenRes.body.result.objectKey) { - logger.error('Token allocation failed:', tokenRes.body) + logger.error('Token分配失败:', tokenRes.body) throw { status: 500, body: { @@ -49,7 +49,7 @@ module.exports = async (query, request) => { }) ).data } catch (error) { - logger.error('LBS fetch failed:', error.message) + logger.error('LBS获取失败:', error.message) throw { status: 500, body: { @@ -61,7 +61,7 @@ module.exports = async (query, request) => { } if (!lbs || !lbs.upload || !lbs.upload[0]) { - logger.error('Invalid LBS response:', lbs) + logger.error('无效的LBS响应:', lbs) throw { status: 500, body: { @@ -95,9 +95,9 @@ module.exports = async (query, request) => { maxBodyLength: Infinity, timeout: 300000, }) - logger.info('Upload success:', filename) + logger.info('上传成功:', filename) } catch (error) { - logger.error('Upload failed:', { + logger.error('上传失败:', { status: error.response?.status, data: error.response?.data, message: error.message, From cfa475bf940848c6422a4f20a05d468edfca5c5d Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:13:50 +0800 Subject: [PATCH 08/16] =?UTF-8?q?chore(deps):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=8C=85=E7=89=88=E6=9C=AC=E5=B9=B6=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E9=9C=80=E8=A6=81=E7=9A=84=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了 md5 依赖包 - 移除了 @neteasecloudmusicapienhanced/unblockmusic-utils 重复依赖 - 移除了 axios 重复依赖 - 移除了 dotenv 重复依赖 - 移除了 music-metadata 重复依赖 - 将开发依赖包版本从固定版本号更新为 ^ 版本范围 - 统一了依赖包版本管理格式 --- package.json | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 3194eac..0385971 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "node_modules/axios", "node_modules/express", "node_modules/express-fileupload", - "node_modules/md5", "node_modules/music-metadata", "node_modules/pac-proxy-agent", "node_modules/qrcode", @@ -66,17 +65,12 @@ "data" ], "dependencies": { - "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.2.2", - "axios": "^1.13.5", "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.2.2", "axios": "^1.13.5", "crypto-js": "^4.2.0", "dotenv": "^17.2.4", - "dotenv": "^17.2.4", "express": "^5.2.1", "express-fileupload": "^1.5.2", - "md5": "^2.3.0", - "music-metadata": "^11.12.0", "music-metadata": "^11.12.0", "node-forge": "^1.3.3", "pac-proxy-agent": "^7.2.0", @@ -93,21 +87,21 @@ "@types/express-fileupload": "^1.5.1", "@types/mocha": "^10.0.10", "@types/node": "25.0.9", - "@typescript-eslint/eslint-plugin": "8.46.3", - "@typescript-eslint/parser": "8.53.0", - "eslint": "9.39.0", - "eslint-config-prettier": "10.1.8", - "eslint-plugin-html": "8.1.3", - "eslint-plugin-prettier": "5.5.5", + "@typescript-eslint/eslint-plugin": "^8.46.3", + "@typescript-eslint/parser": "^8.53.0", + "eslint": "^9.39.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-html": "^8.1.3", + "eslint-plugin-prettier": "^5.5.5", "globals": "^16.5.0", - "husky": "9.1.7", - "intelli-espower-loader": "1.1.0", - "lint-staged": "16.2.7", - "mocha": "11.7.5", + "husky": "^9.1.7", + "intelli-espower-loader": "^1.1.0", + "lint-staged": "^16.2.7", + "mocha": "^11.7.5", "nodemon": "^3.1.11", "pkg": "^5.8.1", - "power-assert": "1.6.1", - "prettier": "3.7.4", - "typescript": "5.9.3" + "power-assert": "^1.6.1", + "prettier": "^3.7.4", + "typescript": "^5.9.3" } } From 84efe5d758ea2f23d6cf8d1d417e7cb52bb4e44a Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:24:45 +0800 Subject: [PATCH 09/16] =?UTF-8?q?chore(deps):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=8C=85=E7=89=88=E6=9C=AC=E5=B9=B6=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=86=97=E4=BD=99=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了 express-fileupload 中的 md5 依赖 - 将多个开发依赖包从固定版本改为范围版本(^) - 更新了 typescript、eslint 等核心依赖的版本声明 - 移除了 charenc、crypt、is-buffer 等冗余依赖包 - 更新了 @typescript-eslint 相关包的版本引用 - 清理了依赖树中的重复和无用条目 --- pnpm-lock.yaml | 57 +++++++++++++------------------------------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d02db53..a0be9e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: express-fileupload: specifier: ^1.5.2 version: 1.5.2 - md5: - specifier: ^2.3.0 - version: 2.3.0 music-metadata: specifier: ^11.12.0 version: 11.12.0 @@ -73,37 +70,37 @@ importers: specifier: 25.0.9 version: 25.0.9 '@typescript-eslint/eslint-plugin': - specifier: 8.46.3 + specifier: ^8.46.3 version: 8.46.3(@typescript-eslint/parser@8.53.0(eslint@9.39.0)(typescript@5.9.3))(eslint@9.39.0)(typescript@5.9.3) '@typescript-eslint/parser': - specifier: 8.53.0 + specifier: ^8.53.0 version: 8.53.0(eslint@9.39.0)(typescript@5.9.3) eslint: - specifier: 9.39.0 + specifier: ^9.39.0 version: 9.39.0 eslint-config-prettier: - specifier: 10.1.8 + specifier: ^10.1.8 version: 10.1.8(eslint@9.39.0) eslint-plugin-html: - specifier: 8.1.3 + specifier: ^8.1.3 version: 8.1.3 eslint-plugin-prettier: - specifier: 5.5.5 + specifier: ^5.5.5 version: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.0))(eslint@9.39.0)(prettier@3.7.4) globals: specifier: ^16.5.0 version: 16.5.0 husky: - specifier: 9.1.7 + specifier: ^9.1.7 version: 9.1.7 intelli-espower-loader: - specifier: 1.1.0 + specifier: ^1.1.0 version: 1.1.0 lint-staged: - specifier: 16.2.7 + specifier: ^16.2.7 version: 16.2.7 mocha: - specifier: 11.7.5 + specifier: ^11.7.5 version: 11.7.5 nodemon: specifier: ^3.1.11 @@ -112,13 +109,13 @@ importers: specifier: ^5.8.1 version: 5.8.1 power-assert: - specifier: 1.6.1 + specifier: ^1.6.1 version: 1.6.1 prettier: - specifier: 3.7.4 + specifier: ^3.7.4 version: 3.7.4 typescript: - specifier: 5.9.3 + specifier: ^5.9.3 version: 5.9.3 packages: @@ -610,9 +607,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - charenc@0.0.2: - resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -710,9 +704,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crypt@0.0.2: - resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} - crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} @@ -1448,9 +1439,6 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} - is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -1651,9 +1639,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - md5@2.3.0: - resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -2899,8 +2884,8 @@ snapshots: '@typescript-eslint/project-service@8.46.3(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) + '@typescript-eslint/types': 8.53.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -3239,8 +3224,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - charenc@0.0.2: {} - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -3340,8 +3323,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - crypt@0.0.2: {} - crypto-js@4.2.0: {} d@1.0.2: @@ -4277,8 +4258,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-buffer@1.1.6: {} - is-callable@1.2.7: {} is-core-module@2.16.1: @@ -4478,12 +4457,6 @@ snapshots: math-intrinsics@1.1.0: {} - md5@2.3.0: - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - is-buffer: 1.1.6 - media-typer@0.3.0: {} media-typer@1.1.0: {} From 83c527af0152252568aa6afae6a9cdb05c055897 Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:42:28 +0800 Subject: [PATCH 10/16] =?UTF-8?q?feat(cloud):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=91=E7=9B=98=E4=B8=8A=E4=BC=A0=E6=A8=A1=E5=BC=8F=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=92=8C=E8=BF=9B=E5=BA=A6=E6=98=BE=E7=A4=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加客户端直传和后端代理两种上传模式选项 - 实现上传进度条和状态显示界面 - 添加文件MD5计算和上传凭证获取功能 - 支持大文件上传和断点续传机制 - 新增cloud/upload/token和cloud/upload/complete接口 - 更新文档说明上传模式和接口使用方法 - 优化上传按钮禁用状态和提示信息显示 --- README.MD | 22 +- module/cloud_upload_complete.js | 73 ++++++ module/cloud_upload_token.js | 109 +++++++++ public/cloud.html | 417 +++++++++++++++++++++++++++++--- public/docs/home.md | 66 ++++- 5 files changed, 637 insertions(+), 50 deletions(-) create mode 100644 module/cloud_upload_complete.js create mode 100644 module/cloud_upload_token.js diff --git a/README.MD b/README.MD index 7ccece5..0b216c8 100644 --- a/README.MD +++ b/README.MD @@ -100,15 +100,15 @@ $ sudo docker run -d -p 3000:3000 ncm-api ## 3. 环境变量 -| 变量名 | 默认值 | 说明 | -| -------------------------- | ------------------------------------ | ------------------------------------------------------------------------------ | +| 变量名 | 默认值 | 说明 | +|----------------------------|--------------------------------------|----------------------------------------------------| | **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。若需要限制,请指定具体域名(例如 `https://example.com`)。 | -| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 | -| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 | -| **ENABLE_GENERAL_UNBLOCK** | `true` | 是否启用全局解灰(推荐开启)。开启后所有歌曲都尝试自动解锁。 | -| **ENABLE_FLAC** | `true` | 是否启用无损音质(FLAC)。 | -| **SELECT_MAX_BR** | `false` | 启用无损音质时,是否选择最高码率音质。 | -| **FOLLOW_SOURCE_ORDER** | `true` | 是否严格按照音源列表顺序进行匹配。 | +| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 | +| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 | +| **ENABLE_GENERAL_UNBLOCK** | `true` | 是否启用全局解灰(推荐开启)。开启后所有歌曲都尝试自动解锁。 | +| **ENABLE_FLAC** | `true` | 是否启用无损音质(FLAC)。 | +| **SELECT_MAX_BR** | `false` | 启用无损音质时,是否选择最高码率音质。 | +| **FOLLOW_SOURCE_ORDER** | `true` | 是否严格按照音源列表顺序进行匹配。 | --- @@ -208,11 +208,11 @@ pnpm test ### SDK 生态 -| 语言 | 作者 | 地址 | 类型 | -| ------ | ------------------------------------------- | ---------------------------------------------------------------------------------------- | ------ | +| 语言 | 作者 | 地址 | 类型 | +|--------|---------------------------------------------|------------------------------------------------------------------------------------------|-----| | Java | [JackuXL](https://github.com/JackuXL) | [NeteaseCloudMusicApi-SDK](https://github.com/JackuXL/NeteaseCloudMusicApi-SDK) | 第三方 | | Java | [1015770492](https://github.com/1015770492) | https://github.com/1015770492/yumbo-music-utils | 第三方 | -| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 | +| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 | ### 依赖此项目的优秀开源项目 diff --git a/module/cloud_upload_complete.js b/module/cloud_upload_complete.js new file mode 100644 index 0000000..10e650b --- /dev/null +++ b/module/cloud_upload_complete.js @@ -0,0 +1,73 @@ +const createOption = require('../util/option.js') + +module.exports = async (query, request) => { + const { + songId, + resourceId, + md5, + filename, + song, + artist, + album, + bitrate = 999000, + } = query + + if (!songId || !resourceId || !md5 || !filename) { + return Promise.reject({ + status: 400, + body: { + code: 400, + msg: '缺少必要参数: songId, resourceId, md5, filename', + }, + }) + } + + const songName = song || filename.replace(/\.[^.]+$/, '') + const ext = filename.includes('.') ? filename.split('.').pop() : 'mp3' + + const res2 = await request( + `/api/upload/cloud/info/v2`, + { + md5: md5, + songid: songId, + filename: filename, + song: songName, + album: album || '未知专辑', + artist: artist || '未知艺术家', + bitrate: String(bitrate), + resourceId: resourceId, + }, + createOption(query), + ) + + if (res2.body.code !== 200) { + return Promise.reject({ + status: res2.status || 500, + body: { + code: res2.body.code || 500, + msg: res2.body.msg || '上传云盘信息失败', + detail: res2.body, + }, + }) + } + + const res3 = await request( + `/api/cloud/pub/v2`, + { + songid: res2.body.songId, + }, + createOption(query), + ) + + return { + status: 200, + body: { + code: 200, + data: { + songId: res2.body.songId, + ...res3.body, + }, + }, + cookie: res2.cookie, + } +} diff --git a/module/cloud_upload_token.js b/module/cloud_upload_token.js new file mode 100644 index 0000000..4238526 --- /dev/null +++ b/module/cloud_upload_token.js @@ -0,0 +1,109 @@ +const createOption = require('../util/option.js') +const crypto = require('crypto') + +module.exports = async (query, request) => { + const { md5, fileSize, filename, bitrate = 999000 } = query + + if (!md5 || !fileSize || !filename) { + return Promise.reject({ + status: 400, + body: { + code: 400, + msg: '缺少必要参数: md5, fileSize, filename', + }, + }) + } + + const ext = filename.includes('.') ? filename.split('.').pop() : 'mp3' + + const checkRes = await request( + `/api/cloud/upload/check`, + { + bitrate: String(bitrate), + ext: '', + length: fileSize, + md5: md5, + songId: '0', + version: 1, + }, + createOption(query), + ) + + const bucket = 'jd-musicrep-privatecloud-audio-public' + const tokenRes = await request( + `/api/nos/token/alloc`, + { + bucket: bucket, + ext: ext, + filename: filename.replace(/\.[^.]+$/, '').replace(/\s/g, '').replace(/\./g, '_'), + local: false, + nos_product: 3, + type: 'audio', + md5: md5, + }, + createOption(query, 'weapi'), + ) + + if (!tokenRes.body.result || !tokenRes.body.result.objectKey) { + return Promise.reject({ + status: 500, + body: { + code: 500, + msg: '获取上传token失败', + detail: tokenRes.body, + }, + }) + } + + const { default: axios } = require('axios') + let lbs + try { + lbs = ( + await axios({ + method: 'get', + url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`, + timeout: 10000, + }) + ).data + } catch (error) { + return Promise.reject({ + status: 500, + body: { + code: 500, + msg: '获取上传服务器地址失败', + detail: error.message, + }, + }) + } + + if (!lbs || !lbs.upload || !lbs.upload[0]) { + return Promise.reject({ + status: 500, + body: { + code: 500, + msg: '获取上传服务器地址无效', + detail: lbs, + }, + }) + } + + return { + status: 200, + body: { + code: 200, + data: { + needUpload: checkRes.body.needUpload, + songId: checkRes.body.songId, + uploadToken: tokenRes.body.result.token, + objectKey: tokenRes.body.result.objectKey, + resourceId: tokenRes.body.result.resourceId, + uploadUrl: `${lbs.upload[0]}/${bucket}/${tokenRes.body.result.objectKey.replace('/', '%2F')}?offset=0&complete=true&version=1.0`, + bucket: bucket, + md5: md5, + fileSize: fileSize, + filename: filename, + }, + }, + cookie: checkRes.cookie, + } +} diff --git a/public/cloud.html b/public/cloud.html index 53bd60a..eda78ce 100644 --- a/public/cloud.html +++ b/public/cloud.html @@ -47,6 +47,59 @@ text-decoration: underline; } + .mode-section { + margin-bottom: 24px; + padding: 16px; + background: #f9f9f9; + border-radius: 8px; + } + + .mode-section label { + display: block; + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 12px; + } + + .mode-options { + display: flex; + gap: 16px; + flex-wrap: wrap; + } + + .mode-option { + display: flex; + align-items: flex-start; + gap: 8px; + cursor: pointer; + } + + .mode-option input[type="radio"] { + margin-top: 3px; + } + + .mode-option-text { + display: flex; + flex-direction: column; + } + + .mode-option-title { + font-size: 14px; + color: #333; + } + + .mode-option-desc { + font-size: 12px; + color: #999; + margin-top: 2px; + } + + .mode-option input[type="radio"]:checked + .mode-option-text .mode-option-title { + color: #333; + font-weight: 500; + } + .upload-section { margin-bottom: 32px; } @@ -72,6 +125,11 @@ display: none; } + .upload-btn.disabled { + background: #ccc; + cursor: not-allowed; + } + .songs-list { list-style: none; } @@ -99,6 +157,74 @@ padding: 20px; color: #666; } + + .progress-section { + margin-bottom: 24px; + display: none; + } + + .progress-section.active { + display: block; + } + + .progress-item { + margin-bottom: 12px; + padding: 12px; + background: #f9f9f9; + border-radius: 6px; + } + + .progress-item .name { + font-size: 14px; + color: #333; + margin-bottom: 8px; + word-break: break-all; + } + + .progress-item .status { + font-size: 12px; + color: #666; + margin-bottom: 6px; + } + + .progress-bar { + height: 6px; + background: #e0e0e0; + border-radius: 3px; + overflow: hidden; + } + + .progress-bar .fill { + height: 100%; + background: #333; + border-radius: 3px; + transition: width 0.3s ease; + width: 0%; + } + + .progress-item.success .fill { + background: #4caf50; + } + + .progress-item.error .fill { + background: #f44336; + } + + .progress-item.error .status { + color: #f44336; + } + + .info-text { + font-size: 12px; + color: #999; + margin-top: 8px; + } + + .warning-text { + font-size: 12px; + color: #e65100; + margin-top: 8px; + } @@ -107,13 +233,36 @@