upppppppp1 commited on
Commit
9c6cb1d
·
verified ·
1 Parent(s): 4830c86

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +184 -30
index.js CHANGED
@@ -12,13 +12,10 @@ const config = {
12
  ],
13
  material_st: [
14
  `userId=5420146991`,
15
- // https://material.kuaishou.com/gateway/ad/shop/material/b/upload/token/generate
16
- // 2026/7/9 07:58:58
17
  `kuaishou.shop.material_st=ChlrdWFpc2hvdS5zaG9wLm1hdGVyaWFsLnN0EsABQutBwizGpcbc4sTVgViBA_h0DBAJ_XRRHwe6azpO8AL-C8_ET3T_lMBmLqQWUOnzMUYXihyZXV14FAz5D8dS19hIq_JtfPcoubu-EMOFA95iUNY3E1RWLxa0HHWtt9uUVNOaISAyQAN-TIkPAasGfL17M6K5GXxeMP4RwGDyr5o_J70OMQ9P9ek-o16iZGmphjhybsvyd7yE46udT3ftGS1xJ2rjG9D7GB5pvdyPqsKvJXa0CM9Naqm3Aoslo7bbGhJU1Gcl9LPZ1oiRrTdeDUWb49siIOHcbjzQCAQ0ZE-R13K22ERvK_NCGB72uxohxd2vlbBpKAUwAQ`
18
  ].join(';'),
19
  im_st: [
20
  `userId=5420146991`,
21
- // https://adim.kuaishou.com/rest/web/upload/getCdnUrl
22
  `kuaishou.customer.service.im_st=Ch9rdWFpc2hvdS5jdXN0b21lci5zZXJ2aWNlLmltLnN0EpABaJxvevAM3sm5rv98aU4YijlfpksO51BaazwuLqP2_N_Pon70bmWaX6ANNjpCdsL6W9CuG0P3LZqt0InCO9iuEfoT4u-AA7neewxMdkJI-GnTpI9NTcDABiuUj2rCu-kGMQCnM4PHSrlitx3olBSLRNZ6nvYlV0M3-FO2ZzabdtmFlkNbDtplH86SzPWvQxjpGhKGsjZn8yUqg0CHOqnb2rpcgroiID30Zh-tHKnwt-C8_xjhoY2vxA6Uk7Xwl4F3rWiOsbCJKAUwAQ`
23
  ].join(';')
24
  }
@@ -38,6 +35,42 @@ const createRequestWithCancel = (cancelToken) => {
38
  }
39
  }
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  // 带重试功能的分片下载函数
42
  async function downloadChunkWithRetry(request, finalUrl, start, end, partNumber, maxRetries = 30) {
43
  let lastError = null
@@ -65,7 +98,6 @@ async function downloadChunkWithRetry(request, finalUrl, start, end, partNumber,
65
  return arrayBuffer
66
 
67
  } catch (error) {
68
- // 如果是取消请求,直接抛出
69
  if (http.isCancel(error)) {
70
  throw error
71
  }
@@ -74,8 +106,6 @@ async function downloadChunkWithRetry(request, finalUrl, start, end, partNumber,
74
  console.warn(`分片 ${partNumber} 下载失败 (尝试 ${attempt}/${maxRetries}):`, error.message)
75
 
76
  if (attempt < maxRetries) {
77
- // 指数退避策略,等待时间逐渐增加
78
- // const waitTime = Math.min(500 * Math.pow(2, attempt - 1), 5000)
79
  const waitTime = 300
80
  console.log(`等待 ${waitTime}ms 后重试分片 ${partNumber}`)
81
  await new Promise(resolve => setTimeout(resolve, waitTime))
@@ -109,7 +139,6 @@ async function uploadChunkWithRetry(request, upload_token, arrayBuffer, partNumb
109
  throw new Error(`分片 ${partNumber} 上传失败`)
110
  }
111
  } catch (error) {
112
- // 如果是取消请求,直接抛出
113
  if (http.isCancel(error)) {
114
  throw error
115
  }
@@ -118,7 +147,6 @@ async function uploadChunkWithRetry(request, upload_token, arrayBuffer, partNumb
118
  console.warn(`分片 ${partNumber} (尝试 ${attempt}/${maxRetries}):`, error.message)
119
 
120
  if (attempt < maxRetries) {
121
- // const waitTime = Math.min(500 * Math.pow(2, attempt - 1), 5000)
122
  const waitTime = 300
123
  console.log(`等待 ${waitTime}ms 后重试上传分片 ${partNumber}`)
124
  await new Promise(resolve => setTimeout(resolve, waitTime))
@@ -135,7 +163,6 @@ async function runWithConcurrency(tasks, maxConcurrency = 50, cancelToken) {
135
  const executing = new Set()
136
 
137
  for (let i = 0; i < tasks.length; i++) {
138
- // 检查是否已取消
139
  if (cancelToken && cancelToken.reason) {
140
  console.log('检测到取消信号,停止创建新任务')
141
  break
@@ -143,7 +170,6 @@ async function runWithConcurrency(tasks, maxConcurrency = 50, cancelToken) {
143
 
144
  const task = tasks[i]
145
 
146
- // 如果当前执行的任务数达到最大并发数,等待其中一个完成
147
  if (executing.size >= maxConcurrency) {
148
  await Promise.race(executing)
149
  }
@@ -160,16 +186,13 @@ async function runWithConcurrency(tasks, maxConcurrency = 50, cancelToken) {
160
  results.push(promise)
161
  }
162
 
163
- // 等待所有剩余任务完成
164
  return Promise.allSettled(results)
165
  }
166
 
167
  app.get('*', async (req, res) => {
168
- // 创建取消令牌
169
  const cancelTokenSource = http.CancelToken.source()
170
  let isRequestCancelled = false
171
 
172
- // 监听连接关闭事件
173
  req.on('close', () => {
174
  if (!res.headersSent) {
175
  console.log('用户断开连接,取消所有请求...')
@@ -179,15 +202,14 @@ app.get('*', async (req, res) => {
179
  })
180
 
181
  try {
182
- const videoUrl = req.path.replace(/^\//, '')
 
183
  if (/favicon.ico/gim.test(videoUrl)) {
184
  return
185
  }
186
 
187
- // 创建带取消令牌的请求实例
188
  const request = createRequestWithCancel(cancelTokenSource.token)
189
 
190
- // 检查是否已取消
191
  if (isRequestCancelled) {
192
  return
193
  }
@@ -201,6 +223,17 @@ app.get('*', async (req, res) => {
201
  const contentLength = headResponse.headers['content-length']
202
  const acceptRanges = headResponse.headers['accept-ranges']
203
 
 
 
 
 
 
 
 
 
 
 
 
204
  const biz = contentLength > config.biz.at(1).size ? config.biz.at(0) : config.biz.at(1)
205
 
206
  if (contentLength > biz.size) {
@@ -212,12 +245,146 @@ app.get('*', async (req, res) => {
212
 
213
  // 检查服务器是否支持范围请求
214
  if (!acceptRanges || acceptRanges === 'none' || !contentLength) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  return res.json({
216
  video_url: videoUrl,
217
- message: '不支持范围请求'
 
 
 
218
  })
219
  }
220
 
 
221
  // 获取上传信息
222
  const upload_info = await request.post('https://material.kuaishou.com/gateway/ad/shop/material/b/upload/token/generate', {
223
  subjectType: 'MERCHANT',
@@ -250,20 +417,16 @@ app.get('*', async (req, res) => {
250
 
251
  tasks.push(() => (async () => {
252
  try {
253
- // 检查是否已取消
254
  if (isRequestCancelled) {
255
  throw new http.Cancel('请求已取消')
256
  }
257
 
258
- // 下载分片(带重试)
259
  const arrayBuffer = await downloadChunkWithRetry(request, finalUrl, start, end, partNumber)
260
 
261
- // 检查是否已取消
262
  if (isRequestCancelled) {
263
  throw new http.Cancel('请求已取消')
264
  }
265
 
266
- // 上传分片(带重试)
267
  const uploadResult = await uploadChunkWithRetry(
268
  request,
269
  token,
@@ -271,9 +434,7 @@ app.get('*', async (req, res) => {
271
  partNumber
272
  )
273
 
274
- // 保存到上传列表
275
  upload_list[partNumber] = uploadResult.checksum
276
-
277
  return uploadResult
278
 
279
  } catch (error) {
@@ -291,23 +452,18 @@ app.get('*', async (req, res) => {
291
  })())
292
  }
293
 
294
- // 3. 并行执行所有分片任务,但限制最大并发数为100
295
  const chunksResults = await runWithConcurrency(tasks, 100, cancelTokenSource)
296
 
297
- // 检查是否已取消
298
  if (isRequestCancelled) {
299
  return
300
  }
301
 
302
- // 处理任务结果
303
  const chunks = chunksResults.map(result =>
304
  result.status === 'fulfilled' ? result.value : result.reason
305
  )
306
 
307
- // 过滤掉取消错误
308
  const validChunks = chunks.filter(chunk => !(chunk instanceof Error && http.isCancel(chunk)))
309
 
310
- // 检查是否所有分片都成功处理
311
  const failedChunks = validChunks.filter(chunk => chunk && chunk.status === 'failed')
312
  if (failedChunks.length > 0) {
313
  return res.status(500).json({
@@ -319,7 +475,6 @@ app.get('*', async (req, res) => {
319
  })
320
  }
321
 
322
- // 检查是否有足够的成功分片
323
  const successChunks = validChunks.filter(chunk => chunk && chunk.status === 'success')
324
  if (successChunks.length === 0) {
325
  return res.status(500).json({
@@ -363,7 +518,6 @@ app.get('*', async (req, res) => {
363
  })
364
 
365
  } catch (error) {
366
- // 如果是取消请求,不发送错误响应
367
  if (http.isCancel(error)) {
368
  console.log('请求已被用户取消')
369
  return
 
12
  ],
13
  material_st: [
14
  `userId=5420146991`,
 
 
15
  `kuaishou.shop.material_st=ChlrdWFpc2hvdS5zaG9wLm1hdGVyaWFsLnN0EsABQutBwizGpcbc4sTVgViBA_h0DBAJ_XRRHwe6azpO8AL-C8_ET3T_lMBmLqQWUOnzMUYXihyZXV14FAz5D8dS19hIq_JtfPcoubu-EMOFA95iUNY3E1RWLxa0HHWtt9uUVNOaISAyQAN-TIkPAasGfL17M6K5GXxeMP4RwGDyr5o_J70OMQ9P9ek-o16iZGmphjhybsvyd7yE46udT3ftGS1xJ2rjG9D7GB5pvdyPqsKvJXa0CM9Naqm3Aoslo7bbGhJU1Gcl9LPZ1oiRrTdeDUWb49siIOHcbjzQCAQ0ZE-R13K22ERvK_NCGB72uxohxd2vlbBpKAUwAQ`
16
  ].join(';'),
17
  im_st: [
18
  `userId=5420146991`,
 
19
  `kuaishou.customer.service.im_st=Ch9rdWFpc2hvdS5jdXN0b21lci5zZXJ2aWNlLmltLnN0EpABaJxvevAM3sm5rv98aU4YijlfpksO51BaazwuLqP2_N_Pon70bmWaX6ANNjpCdsL6W9CuG0P3LZqt0InCO9iuEfoT4u-AA7neewxMdkJI-GnTpI9NTcDABiuUj2rCu-kGMQCnM4PHSrlitx3olBSLRNZ6nvYlV0M3-FO2ZzabdtmFlkNbDtplH86SzPWvQxjpGhKGsjZn8yUqg0CHOqnb2rpcgroiID30Zh-tHKnwt-C8_xjhoY2vxA6Uk7Xwl4F3rWiOsbCJKAUwAQ`
20
  ].join(';')
21
  }
 
35
  }
36
  }
37
 
38
+ // 完整下载文件(用于不支持范围请求的情况)
39
+ async function downloadFullFileWithRetry(request, finalUrl, maxRetries = 30) {
40
+ let lastError = null
41
+
42
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
43
+ try {
44
+ const response = await request.get(finalUrl, {
45
+ responseType: 'arraybuffer'
46
+ })
47
+
48
+ if (response.status !== 200) {
49
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
50
+ }
51
+
52
+ console.log(`完整文件下载成功 (尝试 ${attempt})`)
53
+ return response.data
54
+
55
+ } catch (error) {
56
+ if (http.isCancel(error)) {
57
+ throw error
58
+ }
59
+
60
+ lastError = error
61
+ console.warn(`完整文件下载失败 (尝试 ${attempt}/${maxRetries}):`, error.message)
62
+
63
+ if (attempt < maxRetries) {
64
+ const waitTime = 300
65
+ console.log(`等待 ${waitTime}ms 后重试完整下载`)
66
+ await new Promise(resolve => setTimeout(resolve, waitTime))
67
+ }
68
+ }
69
+ }
70
+
71
+ throw new Error(`完整文件下载失败,已达到最大重试次数: ${lastError.message}`)
72
+ }
73
+
74
  // 带重试功能的分片下载函数
75
  async function downloadChunkWithRetry(request, finalUrl, start, end, partNumber, maxRetries = 30) {
76
  let lastError = null
 
98
  return arrayBuffer
99
 
100
  } catch (error) {
 
101
  if (http.isCancel(error)) {
102
  throw error
103
  }
 
106
  console.warn(`分片 ${partNumber} 下载失败 (尝试 ${attempt}/${maxRetries}):`, error.message)
107
 
108
  if (attempt < maxRetries) {
 
 
109
  const waitTime = 300
110
  console.log(`等待 ${waitTime}ms 后重试分片 ${partNumber}`)
111
  await new Promise(resolve => setTimeout(resolve, waitTime))
 
139
  throw new Error(`分片 ${partNumber} 上传失败`)
140
  }
141
  } catch (error) {
 
142
  if (http.isCancel(error)) {
143
  throw error
144
  }
 
147
  console.warn(`分片 ${partNumber} (尝试 ${attempt}/${maxRetries}):`, error.message)
148
 
149
  if (attempt < maxRetries) {
 
150
  const waitTime = 300
151
  console.log(`等待 ${waitTime}ms 后重试上传分片 ${partNumber}`)
152
  await new Promise(resolve => setTimeout(resolve, waitTime))
 
163
  const executing = new Set()
164
 
165
  for (let i = 0; i < tasks.length; i++) {
 
166
  if (cancelToken && cancelToken.reason) {
167
  console.log('检测到取消信号,停止创建新任务')
168
  break
 
170
 
171
  const task = tasks[i]
172
 
 
173
  if (executing.size >= maxConcurrency) {
174
  await Promise.race(executing)
175
  }
 
186
  results.push(promise)
187
  }
188
 
 
189
  return Promise.allSettled(results)
190
  }
191
 
192
  app.get('*', async (req, res) => {
 
193
  const cancelTokenSource = http.CancelToken.source()
194
  let isRequestCancelled = false
195
 
 
196
  req.on('close', () => {
197
  if (!res.headersSent) {
198
  console.log('用户断开连接,取消所有请求...')
 
202
  })
203
 
204
  try {
205
+ // const videoUrl = req.path.replace(/^\//, '')
206
+ const videoUrl = req.url.replace(/^\//, '')
207
  if (/favicon.ico/gim.test(videoUrl)) {
208
  return
209
  }
210
 
 
211
  const request = createRequestWithCancel(cancelTokenSource.token)
212
 
 
213
  if (isRequestCancelled) {
214
  return
215
  }
 
223
  const contentLength = headResponse.headers['content-length']
224
  const acceptRanges = headResponse.headers['accept-ranges']
225
 
226
+ const contenType = headResponse.headers['content-type']
227
+
228
+ if (/^image|^text|^application/gim.test(contenType)) {
229
+ if (!/application\/octet-stream/gim.test(contenType)) {
230
+ return res.json({
231
+ video_url: videoUrl,
232
+ message: `文件类型不支持 ${contenType}`
233
+ })
234
+ }
235
+ }
236
+
237
  const biz = contentLength > config.biz.at(1).size ? config.biz.at(0) : config.biz.at(1)
238
 
239
  if (contentLength > biz.size) {
 
245
 
246
  // 检查服务器是否支持范围请求
247
  if (!acceptRanges || acceptRanges === 'none' || !contentLength) {
248
+ // 不支持范围请求:完整下载后分片上传
249
+ console.log('服务器不支持范围请求,将完整下载文件...')
250
+
251
+ // 获取上传token
252
+ const upload_info = await request.post('https://material.kuaishou.com/gateway/ad/shop/material/b/upload/token/generate', {
253
+ subjectType: 'MERCHANT',
254
+ bizType: biz.type,
255
+ file: {
256
+ fileName: 'file.mp4',
257
+ fileLength: 1
258
+ }
259
+ }, {
260
+ headers: {
261
+ cookie: config.material_st
262
+ }
263
+ })
264
+ console.log(upload_info.data)
265
+ const token = upload_info.data.data.token
266
+ const upload_list = {}
267
+
268
+ const fileSize = parseInt(contentLength)
269
+ const chunkSize = 20000 * 1024
270
+
271
+ // 完整下载文件
272
+ const fullFileBuffer = await downloadFullFileWithRetry(request, finalUrl)
273
+
274
+ if (isRequestCancelled) {
275
+ return
276
+ }
277
+
278
+ const threadCount = Math.ceil(fileSize / chunkSize)
279
+ console.log(`文件大小: ${fileSize} bytes, 分片数: ${threadCount}`)
280
+
281
+ const tasks = []
282
+ for (let i = 0; i < threadCount; i++) {
283
+ const start = i * chunkSize
284
+ const end = Math.min(start + chunkSize, fileSize)
285
+ const partNumber = i + 1
286
+
287
+ tasks.push(() => (async () => {
288
+ try {
289
+ if (isRequestCancelled) {
290
+ throw new http.Cancel('请求已取消')
291
+ }
292
+
293
+ const chunkBuffer = fullFileBuffer.slice(start, end)
294
+
295
+ const uploadResult = await uploadChunkWithRetry(
296
+ request,
297
+ token,
298
+ chunkBuffer,
299
+ partNumber
300
+ )
301
+
302
+ upload_list[partNumber] = uploadResult.checksum
303
+ return uploadResult
304
+
305
+ } catch (error) {
306
+ if (http.isCancel(error)) {
307
+ console.log(`分片 ${partNumber} 处理被取消`)
308
+ throw error
309
+ }
310
+ console.error(`分片 ${partNumber} 处理失败:`, error.message)
311
+ return {
312
+ part_number: partNumber,
313
+ status: 'failed',
314
+ error: error.message
315
+ }
316
+ }
317
+ })())
318
+ }
319
+
320
+ const chunksResults = await runWithConcurrency(tasks, 100, cancelTokenSource)
321
+
322
+ if (isRequestCancelled) {
323
+ return
324
+ }
325
+
326
+ // 处理任务结果
327
+ const chunks = chunksResults.map(result =>
328
+ result.status === 'fulfilled' ? result.value : result.reason
329
+ )
330
+
331
+ const validChunks = chunks.filter(chunk => !(chunk instanceof Error && http.isCancel(chunk)))
332
+
333
+ const failedChunks = validChunks.filter(chunk => chunk && chunk.status === 'failed')
334
+ if (failedChunks.length > 0) {
335
+ return res.status(500).json({
336
+ video_url: videoUrl,
337
+ message: '部分分片处理失败',
338
+ failed_chunks: failedChunks,
339
+ success_count: validChunks.length - failedChunks.length,
340
+ failed_count: failedChunks.length
341
+ })
342
+ }
343
+
344
+ const successChunks = validChunks.filter(chunk => chunk && chunk.status === 'success')
345
+ if (successChunks.length === 0) {
346
+ return res.status(500).json({
347
+ video_url: videoUrl,
348
+ message: '所有分片处理失败'
349
+ })
350
+ }
351
+
352
+ const upload_result = await request.post(`https://upload.kuaishouzt.com/api/upload/complete?fragment_count=${Object.entries(upload_list).length}&upload_token=${token}`, null, {
353
+ headers: {}
354
+ })
355
+ console.log(upload_result.data)
356
+ console.log(token)
357
+
358
+ if (upload_result.data.result === 1) {
359
+ const upload_result_url = await request.post(`https://adim.kuaishou.com/rest/web/upload/getCdnUrl?bi=103&ac=105369465`, {
360
+ bizType: biz.type,
361
+ subjectType: 'MERCHANT',
362
+ token: token
363
+ }, {
364
+ headers: {
365
+ cookie: config.im_st
366
+ }
367
+ })
368
+ console.log(upload_result_url.data)
369
+ if (upload_result_url.data.result === 1) {
370
+ return res.json({
371
+ url: 'https://v1.adkwai.com/bs2/' + upload_result_url.data.data?.split('?')?.at(0)?.split('/bs2/')?.at(-1),
372
+ message: '文件上传成功',
373
+ total_chunks: successChunks.length
374
+ })
375
+ }
376
+ }
377
+
378
  return res.json({
379
  video_url: videoUrl,
380
+ chunks: successChunks,
381
+ fileSize,
382
+ chunkSize,
383
+ threadCount
384
  })
385
  }
386
 
387
+ // 支持范围请求:继续原有逻辑
388
  // 获取上传信息
389
  const upload_info = await request.post('https://material.kuaishou.com/gateway/ad/shop/material/b/upload/token/generate', {
390
  subjectType: 'MERCHANT',
 
417
 
418
  tasks.push(() => (async () => {
419
  try {
 
420
  if (isRequestCancelled) {
421
  throw new http.Cancel('请求已取消')
422
  }
423
 
 
424
  const arrayBuffer = await downloadChunkWithRetry(request, finalUrl, start, end, partNumber)
425
 
 
426
  if (isRequestCancelled) {
427
  throw new http.Cancel('请求已取消')
428
  }
429
 
 
430
  const uploadResult = await uploadChunkWithRetry(
431
  request,
432
  token,
 
434
  partNumber
435
  )
436
 
 
437
  upload_list[partNumber] = uploadResult.checksum
 
438
  return uploadResult
439
 
440
  } catch (error) {
 
452
  })())
453
  }
454
 
 
455
  const chunksResults = await runWithConcurrency(tasks, 100, cancelTokenSource)
456
 
 
457
  if (isRequestCancelled) {
458
  return
459
  }
460
 
 
461
  const chunks = chunksResults.map(result =>
462
  result.status === 'fulfilled' ? result.value : result.reason
463
  )
464
 
 
465
  const validChunks = chunks.filter(chunk => !(chunk instanceof Error && http.isCancel(chunk)))
466
 
 
467
  const failedChunks = validChunks.filter(chunk => chunk && chunk.status === 'failed')
468
  if (failedChunks.length > 0) {
469
  return res.status(500).json({
 
475
  })
476
  }
477
 
 
478
  const successChunks = validChunks.filter(chunk => chunk && chunk.status === 'success')
479
  if (successChunks.length === 0) {
480
  return res.status(500).json({
 
518
  })
519
 
520
  } catch (error) {
 
521
  if (http.isCancel(error)) {
522
  console.log('请求已被用户取消')
523
  return