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/eslint.config.js b/eslint.config.js index 7e1d03c..2e569fb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -16,7 +16,7 @@ const compat = new FlatCompat({ module.exports = defineConfig([ { languageOptions: { - ecmaVersion: 2018, + ecmaVersion: 2020, sourceType: 'module', parserOptions: { parser: 'babel-eslint', diff --git a/module/cloud.js b/module/cloud.js index 978b7f7..f3c93c8 100644 --- a/module/cloud.js +++ b/module/cloud.js @@ -1,25 +1,26 @@ const uploadPlugin = require('../plugins/songUpload') -const md5 = require('md5') const createOption = require('../util/option.js') const logger = require('../util/logger.js') +const { + isTempFile, + getFileSize, + getFileMd5, + cleanupTempFile, + getFileExtension, + sanitizeFilename, +} = require('../util/fileHelper') + 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() - } + query.songFile.name = Buffer.from(query.songFile.name, 'latin1').toString( 'utf-8', ) - const filename = query.songFile.name - .replace('.' + ext, '') - .replace(/\s/g, '') - .replace(/\./g, '_') + const ext = getFileExtension(query.songFile.name) + const filename = sanitizeFilename(query.songFile.name) const bitrate = 999000 + if (!query.songFile) { return Promise.reject({ status: 500, @@ -29,119 +30,135 @@ 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 - } - const res = await request( - `/api/cloud/upload/check`, - { - bitrate: String(bitrate), - ext: '', - length: query.songFile.size, - md5: query.songFile.md5, - songId: '0', - version: 1, - }, - createOption(query), - ) - let artist = '' - let album = '' - let songName = '' + + const useTemp = isTempFile(query.songFile) + let fileSize = await getFileSize(query.songFile) + let fileMd5 = await getFileMd5(query.songFile) + + query.songFile.md5 = fileMd5 + query.songFile.size = fileSize + try { - const metadata = await mm.parseBuffer( - query.songFile.data, - query.songFile.mimetype, + const res = await request( + `/api/cloud/upload/check`, + { + bitrate: String(bitrate), + ext: '', + length: fileSize, + md5: fileMd5, + songId: '0', + version: 1, + }, + createOption(query), ) - const info = metadata.common - if (info.title) { - songName = info.title - } - if (info.album) { - album = info.album - } - 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) - } - const tokenRes = await request( - `/api/nos/token/alloc`, - { - bucket: '', - ext: ext, - filename: filename, - local: false, - nos_product: 3, - type: 'audio', - md5: query.songFile.md5, - }, - createOption(query), - ) + let artist = '' + let album = '' + let songName = '' - if (res.body.needUpload) { - const uploadInfo = await uploadPlugin(query, request) - // logger.info('uploadInfo', uploadInfo.body.result.resourceId) - } - // logger.info(tokenRes.body.result) - const res2 = await request( - `/api/upload/cloud/info/v2`, - { - md5: query.songFile.md5, - 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), - ) - // logger.info({ res2, privateCloud: res2.body.privateCloud }) - // logger.info(res.body.songId, 'songid') - const res3 = await request( - `/api/cloud/pub/v2`, - { - songid: res2.body.songId, - }, - createOption(query), - ) - // logger.info({ res3 }) - return { - status: 200, - body: { - ...res.body, - ...res3.body, - // ...uploadInfo, - }, - cookie: res.cookie, + try { + let metadata + if (useTemp) { + 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: '获取上传token失败', + detail: tokenRes.body, + }, + }) + } + + 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, + }, + }) + } + + 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, + } + } finally { + if (useTemp) { + await cleanupTempFile(query.songFile.tempFilePath) + } } } diff --git a/module/cloud_upload_complete.js b/module/cloud_upload_complete.js new file mode 100644 index 0000000..d105dff --- /dev/null +++ b/module/cloud_upload_complete.js @@ -0,0 +1,72 @@ +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 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..6e7f3d9 --- /dev/null +++ b/module/cloud_upload_token.js @@ -0,0 +1,111 @@ +const { default: axios } = require('axios') +const createOption = require('../util/option.js') + +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, + }, + }) + } + + 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(/\//g, '%2F')}?offset=0&complete=true&version=1.0`, + bucket: bucket, + md5: md5, + fileSize: fileSize, + filename: filename, + }, + }, + cookie: checkRes.cookie, + } +} diff --git a/module/voice_upload.js b/module/voice_upload.js index dbbd6b7..986fa71 100644 --- a/module/voice_upload.js +++ b/module/voice_upload.js @@ -1,25 +1,26 @@ const { default: axios } = require('axios') +const fs = require('fs') var xml2js = require('xml2js') const createOption = require('../util/option.js') -var parser = new xml2js.Parser(/* options */) +const { getFileExtension, readFileChunk } = require('../util/fileHelper') + +var parser = new xml2js.Parser() + function createDupkey() { - // 格式:3b443c7c-a87f-468d-ba38-46d407aaf23a var s = [] var hexDigits = '0123456789abcdef' for (var i = 0; i < 36; i++) { s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) } - s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010 - s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01 + s[14] = '4' + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) s[8] = s[13] = s[18] = s[23] = '-' return s.join('') } + module.exports = async (query, request) => { - let ext = 'mp3' - if (query.songFile.name.indexOf('flac') > -1) { - ext = 'flac' - } + const ext = getFileExtension(query.songFile.name) const filename = query.songName || query.songFile.name @@ -50,43 +51,58 @@ module.exports = async (query, request) => { createOption(query, 'weapi'), ) - const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F') + const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F') const docId = tokenRes.body.result.docId const res = await axios({ method: 'post', url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`, headers: { 'x-nos-token': tokenRes.body.result.token, - 'X-Nos-Meta-Content-Type': 'audio/mpeg', + 'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg', }, data: null, }) - // return xml + const res2 = await parser.parseStringPromise(res.data) - const fileSize = query.songFile.data.length - const blockSize = 10 * 1024 * 1024 // 10MB + const useTempFile = !!query.songFile.tempFilePath + let fileSize = query.songFile.size + + if (useTempFile) { + const stats = await fs.promises.stat(query.songFile.tempFilePath) + fileSize = stats.size + } + + const blockSize = 10 * 1024 * 1024 let offset = 0 let blockIndex = 1 let etags = [] while (offset < fileSize) { - const chunk = query.songFile.data.slice( - offset, - Math.min(offset + blockSize, fileSize), - ) + let chunk + if (useTempFile) { + chunk = await readFileChunk( + query.songFile.tempFilePath, + offset, + Math.min(blockSize, fileSize - offset), + ) + } else { + chunk = query.songFile.data.slice( + offset, + Math.min(offset + blockSize, fileSize), + ) + } const res3 = await axios({ method: 'put', url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`, headers: { 'x-nos-token': tokenRes.body.result.token, - 'Content-Type': 'audio/mpeg', + 'Content-Type': query.songFile.mimetype || 'audio/mpeg', }, data: chunk, }) - // get etag const etag = res3.headers.etag etags.push(etag) offset += blockSize @@ -101,19 +117,17 @@ module.exports = async (query, request) => { } completeStr += '' - // 文件处理 await axios({ method: 'post', url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`, headers: { 'Content-Type': 'text/plain;charset=UTF-8', - 'X-Nos-Meta-Content-Type': 'audio/mpeg', + 'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg', 'x-nos-token': tokenRes.body.result.token, }, data: completeStr, }) - // preCheck await request( `/api/voice/workbench/voice/batch/upload/preCheck`, { 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" } } diff --git a/plugins/songUpload.js b/plugins/songUpload.js index 0965bd8..379c6a0 100644 --- a/plugins/songUpload.js +++ b/plugins/songUpload.js @@ -1,20 +1,17 @@ const { default: axios } = require('axios') const createOption = require('../util/option.js') const logger = require('../util/logger.js') +const { + getUploadData, + getFileExtension, + sanitizeFilename, +} = require('../util/fileHelper') + 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() - } - const filename = query.songFile.name - .replace('.' + ext, '') - .replace(/\s/g, '') - .replace(/\./g, '_') + const ext = getFileExtension(query.songFile.name) + const filename = sanitizeFilename(query.songFile.name) const bucket = 'jd-musicrep-privatecloud-audio-public' - // 获取key和token + const tokenRes = await request( `/api/nos/token/alloc`, { @@ -29,31 +26,82 @@ module.exports = async (query, request) => { createOption(query, 'weapi'), ) - // 上传 - const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F') + if (!tokenRes.body.result || !tokenRes.body.result.objectKey) { + logger.error('Token分配失败:', tokenRes.body) + throw { + status: 500, + body: { + code: 500, + msg: '获取上传token失败', + detail: tokenRes.body, + }, + } + } + + const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%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获取失败:', error.message) + throw { + status: 500, + body: { + code: 500, + msg: '获取上传服务器地址失败', + detail: error.message, + }, + } + } + + if (!lbs || !lbs.upload || !lbs.upload[0]) { + logger.error('无效的LBS响应:', 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`, headers: { 'x-nos-token': tokenRes.body.result.token, 'Content-MD5': query.songFile.md5, - 'Content-Type': 'audio/mpeg', + 'Content-Type': query.songFile.mimetype || 'audio/mpeg', 'Content-Length': String(query.songFile.size), }, - data: query.songFile.data, + data: getUploadData(query.songFile), maxContentLength: Infinity, maxBodyLength: Infinity, + timeout: 300000, }) + logger.info('上传成功:', filename) } catch (error) { - logger.info('error', error.response) - throw error.response + logger.error('上传失败:', { + 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, diff --git a/plugins/upload.js b/plugins/upload.js index dcd3f5b..3312d5d 100644 --- a/plugins/upload.js +++ b/plugins/upload.js @@ -1,5 +1,7 @@ const { default: axios } = require('axios') const createOption = require('../util/option.js') +const { getUploadData } = require('../util/fileHelper') + module.exports = async (query, request) => { const data = { bucket: 'yyimgs', @@ -10,27 +12,23 @@ module.exports = async (query, request) => { return_body: `{"code":200,"size":"$(ObjectSize)"}`, type: 'other', } - // 获取key和token const res = await request( `/api/nos/token/alloc`, data, createOption(query, 'weapi'), ) - // 上传图片 + const res2 = await axios({ method: 'post', url: `https://nosup-hz1.127.net/yyimgs/${res.body.result.objectKey}?offset=0&complete=true&version=1.0`, headers: { 'x-nos-token': res.body.result.token, - 'Content-Type': 'image/jpeg', + 'Content-Type': query.imgFile.mimetype || 'image/jpeg', }, - data: query.imgFile.data, + data: getUploadData(query.imgFile), }) return { - // ...res.body.result, - // ...res2.data, - // ...res3.body, url_pre: 'https://p1.music.126.net/' + res.body.result.objectKey, imgId: res.body.result.docId, } 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: {} diff --git a/public/cloud.html b/public/cloud.html index 53bd60a..410d2c0 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 @@