@neteasecloudmusicapienhanced/api 4.30.1 → 4.30.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/module/cloud.js CHANGED
@@ -1,25 +1,26 @@
1
1
  const uploadPlugin = require('../plugins/songUpload')
2
- const md5 = require('md5')
3
2
  const createOption = require('../util/option.js')
4
3
  const logger = require('../util/logger.js')
4
+ const {
5
+ isTempFile,
6
+ getFileSize,
7
+ getFileMd5,
8
+ cleanupTempFile,
9
+ getFileExtension,
10
+ sanitizeFilename,
11
+ } = require('../util/fileHelper')
12
+
5
13
  let mm
6
14
  module.exports = async (query, request) => {
7
15
  mm = require('music-metadata')
8
- let ext = 'mp3'
9
- // if (query.songFile.name.indexOf('flac') > -1) {
10
- // ext = 'flac'
11
- // }
12
- if (query.songFile.name.includes('.')) {
13
- ext = query.songFile.name.split('.').pop()
14
- }
16
+
15
17
  query.songFile.name = Buffer.from(query.songFile.name, 'latin1').toString(
16
18
  'utf-8',
17
19
  )
18
- const filename = query.songFile.name
19
- .replace('.' + ext, '')
20
- .replace(/\s/g, '')
21
- .replace(/\./g, '_')
20
+ const ext = getFileExtension(query.songFile.name)
21
+ const filename = sanitizeFilename(query.songFile.name)
22
22
  const bitrate = 999000
23
+
23
24
  if (!query.songFile) {
24
25
  return Promise.reject({
25
26
  status: 500,
@@ -29,119 +30,135 @@ module.exports = async (query, request) => {
29
30
  },
30
31
  })
31
32
  }
32
- if (!query.songFile.md5) {
33
- // 命令行上传没有md5和size信息,需要填充
34
- query.songFile.md5 = md5(query.songFile.data)
35
- query.songFile.size = query.songFile.data.byteLength
36
- }
37
- const res = await request(
38
- `/api/cloud/upload/check`,
39
- {
40
- bitrate: String(bitrate),
41
- ext: '',
42
- length: query.songFile.size,
43
- md5: query.songFile.md5,
44
- songId: '0',
45
- version: 1,
46
- },
47
- createOption(query),
48
- )
49
- let artist = ''
50
- let album = ''
51
- let songName = ''
33
+
34
+ const useTemp = isTempFile(query.songFile)
35
+ let fileSize = await getFileSize(query.songFile)
36
+ let fileMd5 = await getFileMd5(query.songFile)
37
+
38
+ query.songFile.md5 = fileMd5
39
+ query.songFile.size = fileSize
40
+
52
41
  try {
53
- const metadata = await mm.parseBuffer(
54
- query.songFile.data,
55
- query.songFile.mimetype,
42
+ const res = await request(
43
+ `/api/cloud/upload/check`,
44
+ {
45
+ bitrate: String(bitrate),
46
+ ext: '',
47
+ length: fileSize,
48
+ md5: fileMd5,
49
+ songId: '0',
50
+ version: 1,
51
+ },
52
+ createOption(query),
56
53
  )
57
- const info = metadata.common
58
54
 
59
- if (info.title) {
60
- songName = info.title
55
+ let artist = ''
56
+ let album = ''
57
+ let songName = ''
58
+
59
+ try {
60
+ let metadata
61
+ if (useTemp) {
62
+ metadata = await mm.parseFile(query.songFile.tempFilePath)
63
+ } else {
64
+ metadata = await mm.parseBuffer(
65
+ query.songFile.data,
66
+ query.songFile.mimetype,
67
+ )
68
+ }
69
+ const info = metadata.common
70
+ if (info.title) songName = info.title
71
+ if (info.album) album = info.album
72
+ if (info.artist) artist = info.artist
73
+ } catch (error) {
74
+ logger.info('元数据解析错误:', error.message)
61
75
  }
62
- if (info.album) {
63
- album = info.album
76
+
77
+ const tokenRes = await request(
78
+ `/api/nos/token/alloc`,
79
+ {
80
+ bucket: '',
81
+ ext: ext,
82
+ filename: filename,
83
+ local: false,
84
+ nos_product: 3,
85
+ type: 'audio',
86
+ md5: fileMd5,
87
+ },
88
+ createOption(query),
89
+ )
90
+
91
+ if (!tokenRes.body.result || !tokenRes.body.result.resourceId) {
92
+ logger.error('Token分配失败:', tokenRes.body)
93
+ return Promise.reject({
94
+ status: 500,
95
+ body: {
96
+ code: 500,
97
+ msg: '获取上传token失败',
98
+ detail: tokenRes.body,
99
+ },
100
+ })
64
101
  }
65
- if (info.artist) {
66
- artist = info.artist
102
+
103
+ if (res.body.needUpload) {
104
+ logger.info('需要上传,开始上传流程...')
105
+ try {
106
+ const uploadInfo = await uploadPlugin(query, request)
107
+ logger.info('上传完成:', uploadInfo?.body?.result?.resourceId)
108
+ } catch (uploadError) {
109
+ logger.error('上传失败:', uploadError)
110
+ return Promise.reject(uploadError)
111
+ }
112
+ } else {
113
+ logger.info('文件已存在,跳过上传')
67
114
  }
68
- // if (metadata.native.ID3v1) {
69
- // metadata.native.ID3v1.forEach((item) => {
70
- // // logger.info(item.id, item.value)
71
- // if (item.id === 'title') {
72
- // songName = item.value
73
- // }
74
- // if (item.id === 'artist') {
75
- // artist = item.value
76
- // }
77
- // if (item.id === 'album') {
78
- // album = item.value
79
- // }
80
- // })
81
- // // logger.info({
82
- // // songName,
83
- // // album,
84
- // // songName,
85
- // // })
86
- // }
87
- // logger.info({
88
- // songName,
89
- // album,
90
- // songName,
91
- // })
92
- } catch (error) {
93
- logger.info(error)
94
- }
95
- const tokenRes = await request(
96
- `/api/nos/token/alloc`,
97
- {
98
- bucket: '',
99
- ext: ext,
100
- filename: filename,
101
- local: false,
102
- nos_product: 3,
103
- type: 'audio',
104
- md5: query.songFile.md5,
105
- },
106
- createOption(query),
107
- )
108
115
 
109
- if (res.body.needUpload) {
110
- const uploadInfo = await uploadPlugin(query, request)
111
- // logger.info('uploadInfo', uploadInfo.body.result.resourceId)
112
- }
113
- // logger.info(tokenRes.body.result)
114
- const res2 = await request(
115
- `/api/upload/cloud/info/v2`,
116
- {
117
- md5: query.songFile.md5,
118
- songid: res.body.songId,
119
- filename: query.songFile.name,
120
- song: songName || filename,
121
- album: album || '未知专辑',
122
- artist: artist || '未知艺术家',
123
- bitrate: String(bitrate),
124
- resourceId: tokenRes.body.result.resourceId,
125
- },
126
- createOption(query),
127
- )
128
- // logger.info({ res2, privateCloud: res2.body.privateCloud })
129
- // logger.info(res.body.songId, 'songid')
130
- const res3 = await request(
131
- `/api/cloud/pub/v2`,
132
- {
133
- songid: res2.body.songId,
134
- },
135
- createOption(query),
136
- )
137
- // logger.info({ res3 })
138
- return {
139
- status: 200,
140
- body: {
141
- ...res.body,
142
- ...res3.body,
143
- // ...uploadInfo,
144
- },
145
- cookie: res.cookie,
116
+ const res2 = await request(
117
+ `/api/upload/cloud/info/v2`,
118
+ {
119
+ md5: fileMd5,
120
+ songid: res.body.songId,
121
+ filename: query.songFile.name,
122
+ song: songName || filename,
123
+ album: album || '未知专辑',
124
+ artist: artist || '未知艺术家',
125
+ bitrate: String(bitrate),
126
+ resourceId: tokenRes.body.result.resourceId,
127
+ },
128
+ createOption(query),
129
+ )
130
+
131
+ if (res2.body.code !== 200) {
132
+ logger.error('云盘信息上传失败:', res2.body)
133
+ return Promise.reject({
134
+ status: res2.status || 500,
135
+ body: {
136
+ code: res2.body.code || 500,
137
+ msg: res2.body.msg || '上传云盘信息失败',
138
+ detail: res2.body,
139
+ },
140
+ })
141
+ }
142
+
143
+ const res3 = await request(
144
+ `/api/cloud/pub/v2`,
145
+ {
146
+ songid: res2.body.songId,
147
+ },
148
+ createOption(query),
149
+ )
150
+
151
+ return {
152
+ status: 200,
153
+ body: {
154
+ ...res.body,
155
+ ...res3.body,
156
+ },
157
+ cookie: res.cookie,
158
+ }
159
+ } finally {
160
+ if (useTemp) {
161
+ await cleanupTempFile(query.songFile.tempFilePath)
162
+ }
146
163
  }
147
164
  }
@@ -0,0 +1,72 @@
1
+ const createOption = require('../util/option.js')
2
+
3
+ module.exports = async (query, request) => {
4
+ const {
5
+ songId,
6
+ resourceId,
7
+ md5,
8
+ filename,
9
+ song,
10
+ artist,
11
+ album,
12
+ bitrate = 999000,
13
+ } = query
14
+
15
+ if (!songId || !resourceId || !md5 || !filename) {
16
+ return Promise.reject({
17
+ status: 400,
18
+ body: {
19
+ code: 400,
20
+ msg: '缺少必要参数: songId, resourceId, md5, filename',
21
+ },
22
+ })
23
+ }
24
+
25
+ const songName = song || filename.replace(/\.[^.]+$/, '')
26
+
27
+ const res2 = await request(
28
+ `/api/upload/cloud/info/v2`,
29
+ {
30
+ md5: md5,
31
+ songid: songId,
32
+ filename: filename,
33
+ song: songName,
34
+ album: album || '未知专辑',
35
+ artist: artist || '未知艺术家',
36
+ bitrate: String(bitrate),
37
+ resourceId: resourceId,
38
+ },
39
+ createOption(query),
40
+ )
41
+
42
+ if (res2.body.code !== 200) {
43
+ return Promise.reject({
44
+ status: res2.status || 500,
45
+ body: {
46
+ code: res2.body.code || 500,
47
+ msg: res2.body.msg || '上传云盘信息失败',
48
+ detail: res2.body,
49
+ },
50
+ })
51
+ }
52
+
53
+ const res3 = await request(
54
+ `/api/cloud/pub/v2`,
55
+ {
56
+ songid: res2.body.songId,
57
+ },
58
+ createOption(query),
59
+ )
60
+
61
+ return {
62
+ status: 200,
63
+ body: {
64
+ code: 200,
65
+ data: {
66
+ songId: res2.body.songId,
67
+ ...res3.body,
68
+ },
69
+ },
70
+ cookie: res2.cookie,
71
+ }
72
+ }
@@ -0,0 +1,111 @@
1
+ const { default: axios } = require('axios')
2
+ const createOption = require('../util/option.js')
3
+
4
+ module.exports = async (query, request) => {
5
+ const { md5, fileSize, filename, bitrate = 999000 } = query
6
+
7
+ if (!md5 || !fileSize || !filename) {
8
+ return Promise.reject({
9
+ status: 400,
10
+ body: {
11
+ code: 400,
12
+ msg: '缺少必要参数: md5, fileSize, filename',
13
+ },
14
+ })
15
+ }
16
+
17
+ const ext = filename.includes('.') ? filename.split('.').pop() : 'mp3'
18
+
19
+ const checkRes = await request(
20
+ `/api/cloud/upload/check`,
21
+ {
22
+ bitrate: String(bitrate),
23
+ ext: '',
24
+ length: fileSize,
25
+ md5: md5,
26
+ songId: '0',
27
+ version: 1,
28
+ },
29
+ createOption(query),
30
+ )
31
+
32
+ const bucket = 'jd-musicrep-privatecloud-audio-public'
33
+ const tokenRes = await request(
34
+ `/api/nos/token/alloc`,
35
+ {
36
+ bucket: bucket,
37
+ ext: ext,
38
+ filename: filename
39
+ .replace(/\.[^.]+$/, '')
40
+ .replace(/\s/g, '')
41
+ .replace(/\./g, '_'),
42
+ local: false,
43
+ nos_product: 3,
44
+ type: 'audio',
45
+ md5: md5,
46
+ },
47
+ createOption(query, 'weapi'),
48
+ )
49
+
50
+ if (!tokenRes.body.result || !tokenRes.body.result.objectKey) {
51
+ return Promise.reject({
52
+ status: 500,
53
+ body: {
54
+ code: 500,
55
+ msg: '获取上传token失败',
56
+ detail: tokenRes.body,
57
+ },
58
+ })
59
+ }
60
+
61
+ let lbs
62
+ try {
63
+ lbs = (
64
+ await axios({
65
+ method: 'get',
66
+ url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
67
+ timeout: 10000,
68
+ })
69
+ ).data
70
+ } catch (error) {
71
+ return Promise.reject({
72
+ status: 500,
73
+ body: {
74
+ code: 500,
75
+ msg: '获取上传服务器地址失败',
76
+ detail: error.message,
77
+ },
78
+ })
79
+ }
80
+
81
+ if (!lbs || !lbs.upload || !lbs.upload[0]) {
82
+ return Promise.reject({
83
+ status: 500,
84
+ body: {
85
+ code: 500,
86
+ msg: '获取上传服务器地址无效',
87
+ detail: lbs,
88
+ },
89
+ })
90
+ }
91
+
92
+ return {
93
+ status: 200,
94
+ body: {
95
+ code: 200,
96
+ data: {
97
+ needUpload: checkRes.body.needUpload,
98
+ songId: checkRes.body.songId,
99
+ uploadToken: tokenRes.body.result.token,
100
+ objectKey: tokenRes.body.result.objectKey,
101
+ resourceId: tokenRes.body.result.resourceId,
102
+ uploadUrl: `${lbs.upload[0]}/${bucket}/${tokenRes.body.result.objectKey.replace(/\//g, '%2F')}?offset=0&complete=true&version=1.0`,
103
+ bucket: bucket,
104
+ md5: md5,
105
+ fileSize: fileSize,
106
+ filename: filename,
107
+ },
108
+ },
109
+ cookie: checkRes.cookie,
110
+ }
111
+ }
@@ -0,0 +1,30 @@
1
+ // 评论统计数据
2
+ // type: 0=歌曲 1=MV 2=歌单 3=专辑 4=电台节目 5=视频 6=动态 7=电台
3
+ // ids: 资源 ID 列表,多个用逗号分隔,如 "123,456"
4
+ const { resourceTypeMap } = require('../util/config.json')
5
+ const createOption = require('../util/option.js')
6
+
7
+ // 从 resourceTypeMap 的前缀值中提取网易云内部资源类型编号
8
+ // 例如 "R_SO_4_" -> "4", "A_DR_14_" -> "14"
9
+ const resourceTypeIdMap = Object.fromEntries(
10
+ Object.entries(resourceTypeMap).map(([key, prefix]) => [
11
+ key,
12
+ prefix.replace(/_$/, '').split('_').pop(),
13
+ ]),
14
+ )
15
+
16
+ module.exports = (query, request) => {
17
+ const ids = String(query.ids || query.id || '')
18
+ .split(',')
19
+ .map((id) => id.trim())
20
+ .filter(Boolean)
21
+
22
+ return request(
23
+ `/api/resource/commentInfo/list`,
24
+ {
25
+ resourceType: resourceTypeIdMap[String(query.type || 0)],
26
+ resourceIds: JSON.stringify(ids),
27
+ },
28
+ createOption(query, 'weapi'),
29
+ )
30
+ }
@@ -1,77 +1,5 @@
1
- // GD音乐台get(适配SPlayer的UNM-Server)
2
- // 感谢来自GD Studio的开发API
3
- // https://music.gdstudio.xyz/
4
-
5
- const createOption = require('../util/option.js')
1
+ // 夹带私货的东西就不要放在这里了
6
2
 
7
3
  module.exports = async (query, request) => {
8
- try {
9
- const { id, br = '320' } = query
10
- if (!id) {
11
- return {
12
- status: 400,
13
- body: {
14
- code: 400,
15
- message: '缺少必要参数 id',
16
- data: [],
17
- },
18
- }
19
- }
20
- const validBR = ['128', '192', '320', '740', '999']
21
- // const covertBR = ['128000', '192000', '320000','740000', '999000']
22
- if (!validBR.includes(br)) {
23
- return {
24
- status: 400,
25
- body: {
26
- code: 400,
27
- message: '无效音质参数',
28
- allowed_values: validBR,
29
- data: [],
30
- },
31
- }
32
- }
33
-
34
- const apiUrl = new URL('https://music-api.gdstudio.xyz/api.php')
35
- apiUrl.searchParams.append('types', 'url')
36
- apiUrl.searchParams.append('id', id)
37
- apiUrl.searchParams.append('br', br)
38
-
39
- const response = await fetch(apiUrl.toString())
40
- if (!response.ok) throw new Error(`API 响应状态: ${response.status}`)
41
- const result = await response.json()
42
-
43
- // 代理逻辑
44
- const useProxy = process.env.ENABLE_PROXY || false
45
- const proxy = process.env.PROXY_URL
46
- if (useProxy && result.url && result.url.includes('kuwo')) {
47
- result.proxyUrl = proxy + result.url.replace(/^http:\/\//, 'http/')
48
- }
49
-
50
- return {
51
- status: 200,
52
- body: {
53
- code: 200,
54
- message: '请求成功',
55
- data: {
56
- id,
57
- br,
58
- url: result.url,
59
- ...(proxy && result.proxyUrl ? { proxyUrl: result.proxyUrl } : {}),
60
- },
61
- },
62
- }
63
- } catch (error) {
64
- console.error('Error in song_url_ncmget:', error)
65
- return {
66
- status: 500,
67
- body: {
68
- code: 500,
69
- message: '服务器处理请求失败',
70
- ...(process.env.NODE_ENV === 'development'
71
- ? { error: error.message }
72
- : {}),
73
- data: [],
74
- },
75
- }
76
- }
4
+ return { status: 200, body: { code: 200, data: [] } }
77
5
  }
@@ -1,25 +1,26 @@
1
1
  const { default: axios } = require('axios')
2
+ const fs = require('fs')
2
3
  var xml2js = require('xml2js')
3
4
 
4
5
  const createOption = require('../util/option.js')
5
- var parser = new xml2js.Parser(/* options */)
6
+ const { getFileExtension, readFileChunk } = require('../util/fileHelper')
7
+
8
+ var parser = new xml2js.Parser()
9
+
6
10
  function createDupkey() {
7
- // 格式:3b443c7c-a87f-468d-ba38-46d407aaf23a
8
11
  var s = []
9
12
  var hexDigits = '0123456789abcdef'
10
13
  for (var i = 0; i < 36; i++) {
11
14
  s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
12
15
  }
13
- s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
14
- s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01
16
+ s[14] = '4'
17
+ s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1)
15
18
  s[8] = s[13] = s[18] = s[23] = '-'
16
19
  return s.join('')
17
20
  }
21
+
18
22
  module.exports = async (query, request) => {
19
- let ext = 'mp3'
20
- if (query.songFile.name.indexOf('flac') > -1) {
21
- ext = 'flac'
22
- }
23
+ const ext = getFileExtension(query.songFile.name)
23
24
  const filename =
24
25
  query.songName ||
25
26
  query.songFile.name
@@ -50,43 +51,58 @@ module.exports = async (query, request) => {
50
51
  createOption(query, 'weapi'),
51
52
  )
52
53
 
53
- const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
54
+ const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
54
55
  const docId = tokenRes.body.result.docId
55
56
  const res = await axios({
56
57
  method: 'post',
57
58
  url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
58
59
  headers: {
59
60
  'x-nos-token': tokenRes.body.result.token,
60
- 'X-Nos-Meta-Content-Type': 'audio/mpeg',
61
+ 'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
61
62
  },
62
63
  data: null,
63
64
  })
64
- // return xml
65
+
65
66
  const res2 = await parser.parseStringPromise(res.data)
66
67
 
67
- const fileSize = query.songFile.data.length
68
- const blockSize = 10 * 1024 * 1024 // 10MB
68
+ const useTempFile = !!query.songFile.tempFilePath
69
+ let fileSize = query.songFile.size
70
+
71
+ if (useTempFile) {
72
+ const stats = await fs.promises.stat(query.songFile.tempFilePath)
73
+ fileSize = stats.size
74
+ }
75
+
76
+ const blockSize = 10 * 1024 * 1024
69
77
  let offset = 0
70
78
  let blockIndex = 1
71
79
 
72
80
  let etags = []
73
81
 
74
82
  while (offset < fileSize) {
75
- const chunk = query.songFile.data.slice(
76
- offset,
77
- Math.min(offset + blockSize, fileSize),
78
- )
83
+ let chunk
84
+ if (useTempFile) {
85
+ chunk = await readFileChunk(
86
+ query.songFile.tempFilePath,
87
+ offset,
88
+ Math.min(blockSize, fileSize - offset),
89
+ )
90
+ } else {
91
+ chunk = query.songFile.data.slice(
92
+ offset,
93
+ Math.min(offset + blockSize, fileSize),
94
+ )
95
+ }
79
96
 
80
97
  const res3 = await axios({
81
98
  method: 'put',
82
99
  url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
83
100
  headers: {
84
101
  'x-nos-token': tokenRes.body.result.token,
85
- 'Content-Type': 'audio/mpeg',
102
+ 'Content-Type': query.songFile.mimetype || 'audio/mpeg',
86
103
  },
87
104
  data: chunk,
88
105
  })
89
- // get etag
90
106
  const etag = res3.headers.etag
91
107
  etags.push(etag)
92
108
  offset += blockSize
@@ -101,19 +117,17 @@ module.exports = async (query, request) => {
101
117
  }
102
118
  completeStr += '</CompleteMultipartUpload>'
103
119
 
104
- // 文件处理
105
120
  await axios({
106
121
  method: 'post',
107
122
  url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
108
123
  headers: {
109
124
  'Content-Type': 'text/plain;charset=UTF-8',
110
- 'X-Nos-Meta-Content-Type': 'audio/mpeg',
125
+ 'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
111
126
  'x-nos-token': tokenRes.body.result.token,
112
127
  },
113
128
  data: completeStr,
114
129
  })
115
130
 
116
- // preCheck
117
131
  await request(
118
132
  `/api/voice/workbench/voice/batch/upload/preCheck`,
119
133
  {