video-download.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. function registerVideoDownloadRoutes(app, deps) {
  2. const {
  3. path,
  4. fs,
  5. crypto,
  6. Readable,
  7. Transform,
  8. pipeline,
  9. dataVideoDir,
  10. downloadTasks,
  11. normalizeRemoteUrls,
  12. ensureVideoFilename,
  13. ensureUniqueVideoFilename,
  14. fetchRemoteVideoResponse,
  15. createManagedVideoEntry,
  16. readManifest,
  17. writeManifest,
  18. removeFileIfExists,
  19. } = deps;
  20. app.post('/api/download/video', (req, res) => {
  21. const {
  22. url,
  23. urls,
  24. filename,
  25. title,
  26. description,
  27. tags,
  28. thumbnail,
  29. duration,
  30. resolution,
  31. awemeId,
  32. authorName,
  33. } = req.body || {};
  34. if (!url || typeof url !== 'string') {
  35. return res.status(400).json({ error: '缺少 url 参数' });
  36. }
  37. const candidateUrls = normalizeRemoteUrls(url, urls);
  38. if (candidateUrls.length === 0) {
  39. return res.status(400).json({ error: '无效的视频地址' });
  40. }
  41. const safeFilename = ensureUniqueVideoFilename(ensureVideoFilename(filename || title || awemeId || 'douyin-video.mp4', url));
  42. const filePath = path.join(dataVideoDir, safeFilename);
  43. const taskId = `DL-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
  44. const task = {
  45. id: taskId,
  46. status: 'pending',
  47. progress: 0,
  48. downloadedBytes: 0,
  49. totalBytes: 0,
  50. filename: safeFilename,
  51. created_at: new Date().toISOString(),
  52. };
  53. downloadTasks.set(taskId, task);
  54. res.json({ success: true, taskId, filename: safeFilename });
  55. (async () => {
  56. try {
  57. task.status = 'downloading';
  58. console.log(`📥 开始下载远程视频: ${safeFilename} ← ${candidateUrls[0]}`);
  59. const { url: resolvedUrl, response } = await fetchRemoteVideoResponse(candidateUrls, {
  60. Accept: '*/*',
  61. 'User-Agent': 'Mozilla/5.0',
  62. Referer: 'https://www.douyin.com/',
  63. Origin: 'https://www.douyin.com',
  64. });
  65. task.sourceUrl = resolvedUrl;
  66. const totalBytes = Number.parseInt(response.headers.get('content-length') || '0', 10) || 0;
  67. task.totalBytes = totalBytes;
  68. let downloadedBytes = 0;
  69. let chunkCount = 0;
  70. const progressStream = new Transform({
  71. transform(chunk, encoding, callback) {
  72. downloadedBytes += chunk.length;
  73. chunkCount += 1;
  74. task.downloadedBytes = downloadedBytes;
  75. task.progress = totalBytes > 0
  76. ? Math.min(99, Math.round((downloadedBytes / totalBytes) * 100))
  77. : Math.min(95, Math.max(task.progress || 0, Math.min(95, chunkCount)));
  78. callback(null, chunk);
  79. },
  80. });
  81. await pipeline(
  82. Readable.fromWeb(response.body),
  83. progressStream,
  84. fs.createWriteStream(filePath),
  85. );
  86. const stat = fs.statSync(filePath);
  87. const manifest = readManifest();
  88. const videoEntry = createManagedVideoEntry({
  89. filename: safeFilename,
  90. title,
  91. description,
  92. tags,
  93. thumbnail,
  94. duration,
  95. resolution,
  96. awemeId,
  97. authorName,
  98. size: stat.size,
  99. });
  100. manifest.push(videoEntry);
  101. writeManifest(manifest);
  102. task.status = 'completed';
  103. task.progress = 100;
  104. task.completed_at = new Date().toISOString();
  105. task.video = videoEntry;
  106. console.log(`✅ 远程视频下载完成: ${safeFilename} (${(stat.size / 1024 / 1024).toFixed(1)}MB)`);
  107. } catch (error) {
  108. removeFileIfExists(filePath);
  109. task.status = 'failed';
  110. task.error = error.message;
  111. task.failed_at = new Date().toISOString();
  112. console.error('❌ 远程视频下载失败:', error);
  113. }
  114. })();
  115. });
  116. app.get('/api/download/video/:taskId', (req, res) => {
  117. const task = downloadTasks.get(req.params.taskId);
  118. if (!task) {
  119. return res.status(404).json({ error: '未找到下载任务' });
  120. }
  121. res.json(task);
  122. });
  123. }
  124. module.exports = { registerVideoDownloadRoutes };