tools_batch_files 1.0.37 → 1.0.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tools_batch_files",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "description": "批处理视频工具",
5
5
  "keywords": [
6
6
  "utils",
@@ -10,11 +10,11 @@
10
10
  ],
11
11
  "main": "index.js",
12
12
  "bin": {
13
- "tbf": "./index.js",
14
13
  "tbfp": "./src/photoBatch.js",
14
+ "tbfps": "./src/photoFn/photoBatch.js",
15
15
  "tbfa": "./src/audioFn/audioBatch.js",
16
+ "tbfae": "./src/audioFn/audioExample.js",
16
17
  "tbfv": "./src/videoFn/videoBatch.js",
17
- "tbfb": "./src/photoBatchBack.js",
18
18
  "tbfre": "./src/removeFailVideo.js"
19
19
  },
20
20
  "scripts": {
@@ -34,7 +34,6 @@
34
34
  "xlsx": "^0.18.5"
35
35
  },
36
36
  "files": [
37
- "index.js",
38
37
  "imgs",
39
38
  "vocal_print",
40
39
  "点我批量上传视频.bat",
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+ const { logger } = require("../../utils/logger");
3
+ const {
4
+ findFileInDir,
5
+ logFileSize,
6
+ ensureDirSync,
7
+ removeDirectory,
8
+ } = require("../../utils/settleFiles");
9
+ const ffmpeg = require("fluent-ffmpeg");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const fs_asnyc = require("fs").promises;
13
+ const archiver = require("archiver");
14
+ const axios = require("axios");
15
+ const FormData = require("form-data");
16
+ const { Queue } = require("./queue");
17
+
18
+ const ORIGIN_FILE_DIR = "audio";
19
+ const WM_AUDIO_DIR = "watermark_audio";
20
+ const WAV_AUDIO_DIR = "wav_audio";
21
+ const SOURCE_AUDIO_DIR = "source_audio";
22
+ const ZIP_FILES_DIR = "zip";
23
+ //水印
24
+ const watermarkAudioPath = path.join(
25
+ __dirname,
26
+ "..",
27
+ "..",
28
+ "vocal_print",
29
+ "mz.mp3"
30
+ );
31
+ const longWatermarkAudioPath = path.join(
32
+ __dirname,
33
+ "..",
34
+ "..",
35
+ "vocal_print",
36
+ "800a.mp3"
37
+ );
38
+
39
+ const maxRetries = 1; // 最大重试次数
40
+
41
+ /**
42
+ * 声纹
43
+ */
44
+ const watermarkAudio = (indexFilePath, originFilePath, fileName, duration) => {
45
+ //存放声纹文件夹
46
+ const outputDir = path.join(indexFilePath, WM_AUDIO_DIR);
47
+ const outputFile = path.join(outputDir, fileName);
48
+ let vocalPath = "";
49
+
50
+ ensureDirSync(outputDir);
51
+ logger("duration" + duration);
52
+
53
+ // 确保输出时长与源音频保持一致,方案改为截取声纹音频了。
54
+ const complexFilter = [
55
+ `[0:a]volume=1[a0]`, // 设置原音频音量为1,重命名为a0
56
+ `[1:a]atrim=0:duration=${duration},volume=1.5[a1]`, // 对声纹音频流进行截取操作,并设置音量,重命名为a1
57
+ `[a0][a1]amix=inputs=2[a]`, // 合并两个音频流为一个输出流,重命名为a
58
+ `[a]volume=3.0`, // 设置输出音频的音量
59
+ ];
60
+
61
+ if (duration <= 5) {
62
+ //直接合并,放在末尾,下次补上
63
+ vocalPath = watermarkAudioPath;
64
+ } else if (duration > 5 && duration < 30) {
65
+ // 5s开始/ 每隔10s出现一次
66
+ vocalPath = watermarkAudioPath;
67
+
68
+ const totalCount = Math.floor((duration + 5) / 10);
69
+ for (let i = 1; i <= totalCount; i++) {
70
+ if (i === 1) {
71
+ complexFilter.push(`[1:a]adelay=5000|2000,volume=2.0[a${i}]`);
72
+ } else {
73
+ complexFilter.push(
74
+ `[1:a]adelay=${5000 + 10000 * (i - 1)}|2000,volume=2.0[a${i}]`
75
+ );
76
+ }
77
+ }
78
+ } else {
79
+ //30以上 10s开始 每隔10s出现一次
80
+ vocalPath = longWatermarkAudioPath;
81
+ }
82
+
83
+ return new Promise((resolve, reject) => {
84
+ const watermarkCommand = ffmpeg();
85
+ // 打声纹
86
+ watermarkCommand
87
+ .input(originFilePath)
88
+ .input(vocalPath) // 声纹音频文件
89
+
90
+ .complexFilter(complexFilter.join(";"))
91
+ .output(outputFile)
92
+ .on("error", (err) => {
93
+ logger("添加声纹出错: " + err);
94
+ reject(err);
95
+ })
96
+ .on("end", () => {
97
+ logger("添加声纹完成: " + outputFile);
98
+ resolve();
99
+ })
100
+ .run();
101
+ });
102
+ };
103
+
104
+ /**
105
+ * 生成wav
106
+ */
107
+ const wavAudio = (indexFilePath, originFilePath, fileName) => {
108
+ //存放wav文件夹
109
+ const outputDir = path.join(indexFilePath, WAV_AUDIO_DIR);
110
+ ensureDirSync(outputDir);
111
+
112
+ const outputFile = path.join(outputDir, fileName.replace(".mp3", ".wav"));
113
+
114
+ return new Promise((resolve, reject) => {
115
+ const watermarkCommand = ffmpeg(originFilePath);
116
+ watermarkCommand
117
+ .output(outputFile)
118
+ .on("error", (err) => {
119
+ logger("生成wav出错: " + err);
120
+ reject();
121
+ })
122
+ .on("end", () => {
123
+ logger("wav文件生成完成: " + outputFile);
124
+ resolve();
125
+ })
126
+ .run();
127
+ });
128
+ };
129
+
130
+ /**
131
+ * 打包物料
132
+ */
133
+ const archiveZip = (fileName, inputPath, originFilePath) => {
134
+ const zipDir = path.join(inputPath, "zip");
135
+ const timestamp = new Date().getTime();
136
+
137
+ ensureDirSync(zipDir);
138
+ const zipStream = fs.createWriteStream(
139
+ path.join(zipDir, `package${timestamp}.zip`)
140
+ );
141
+ const archive = archiver("zip", {
142
+ zlib: { level: 9 },
143
+ });
144
+
145
+ return new Promise((resolve, reject) => {
146
+ zipStream.on("close", function () {
147
+ logger("压缩数据:" + archive.pointer() + " total bytes");
148
+ logger(
149
+ "完成归档archiver has been finalized and the output file descriptor has closed."
150
+ );
151
+ resolve();
152
+ });
153
+
154
+ archive.on("warning", function (err) {
155
+ if (err.code === "ENOENT") {
156
+ logger("压缩-warning:" + err);
157
+ } else {
158
+ throw err;
159
+ }
160
+ });
161
+
162
+ archive.on("error", function (err) {
163
+ logger("压缩失败!" + err);
164
+ reject();
165
+ });
166
+
167
+ archive.pipe(zipStream);
168
+ const directories = [WAV_AUDIO_DIR, WM_AUDIO_DIR];
169
+
170
+ directories.forEach((dir) => {
171
+ const dirPath = path.join(inputPath, dir);
172
+ archive.directory(dirPath, dir);
173
+ });
174
+
175
+ archive.file(originFilePath, {
176
+ name: path.join(SOURCE_AUDIO_DIR, fileName),
177
+ });
178
+ // 完成归档
179
+ archive.finalize();
180
+ });
181
+ };
182
+
183
+ /**
184
+ * 获取 元数据
185
+ */
186
+ const getMetadata = async (originFilePath, { title, keyword, anotherId }) => {
187
+ //源音频数据
188
+ const photoMetadataComand = ffmpeg(originFilePath);
189
+
190
+ const metaDataParams = {
191
+ userid: 192375294,
192
+ username: "张杰",
193
+ title,
194
+ keyword,
195
+ en_keyword: "",
196
+ en_title: "",
197
+ size: "",
198
+ pr: 0,
199
+ sampling: "", // 采样率
200
+ duration: "",
201
+ tag_ids: "3",
202
+ source_from: "sound_1",
203
+ anotherId,
204
+ };
205
+
206
+ await new Promise((resolve, reject) => {
207
+ photoMetadataComand.ffprobe(function (err, metadata) {
208
+ if (metadata) {
209
+ const formatStream = metadata.format;
210
+ const audioStream = metadata.streams.find(
211
+ (s) => s.codec_type === "audio"
212
+ );
213
+
214
+ metaDataParams.size = formatStream.size;
215
+ metaDataParams.duration = audioStream.duration;
216
+ metaDataParams.sampling = audioStream.sample_rate;
217
+
218
+ resolve();
219
+ } else {
220
+ reject(err);
221
+ }
222
+ });
223
+ });
224
+
225
+ return metaDataParams;
226
+ };
227
+
228
+ /**
229
+ * 接口
230
+ */
231
+ const postData = (dataParams, indexFilePath, index) => {
232
+ const formData = new FormData();
233
+
234
+ const zipFiles = fs
235
+ .readdirSync(path.join(indexFilePath, ZIP_FILES_DIR))
236
+ .find((file) => file.endsWith(".zip"));
237
+
238
+ const packageZip = path.join(indexFilePath, ZIP_FILES_DIR, zipFiles);
239
+
240
+ formData.append("file", fs.createReadStream(packageZip));
241
+ for (const key in dataParams) {
242
+ if (Object.hasOwnProperty.call(dataParams, key)) {
243
+ const value = dataParams[key];
244
+ formData.append(key, value);
245
+ }
246
+ }
247
+
248
+ logger(`第${index}条等待接口返回结果……`);
249
+
250
+ // return axios.post("http://192.168.102.61:9999/upload/sound", formData, {
251
+ // return axios.post("http://192.168.101.149:9999/upload/sound", formData, {
252
+ return axios.post("http://example/upload/sound", formData, {
253
+ headers: {
254
+ "Content-Type": "multipart/form-data",
255
+ },
256
+ timeout: 300000,
257
+ });
258
+ };
259
+
260
+ /**
261
+ * 接口重试机制
262
+ */
263
+ async function postDataWithRetry(dataParams, indexFilePath, index) {
264
+ let retryCount = 0; // 当前重试次数
265
+
266
+ while (retryCount < maxRetries) {
267
+ try {
268
+ const resData = await postData(dataParams, indexFilePath, index);
269
+ if (resData.data.code === 200) {
270
+ logger("请求成功!");
271
+ logger(resData.data.code);
272
+
273
+ removeDirectory(indexFilePath);
274
+ return;
275
+ } else if (resData.data.code === 300) {
276
+ // 重复上传,不捕获此错误
277
+ logger(resData.data.code);
278
+ logger(`第${index}条文件-${index}重复上传!`);
279
+ removeDirectory(indexFilePath);
280
+ return;
281
+ } else {
282
+ logger(`请求失败,重试中... (${retryCount + 1}/${maxRetries})`);
283
+ logger(`请求code!==200: ${resData.data.code}${resData.data.msg}`);
284
+ // 延时等待一段时间后再进行重试
285
+ await new Promise((resolve) => setTimeout(resolve, 100)); // 等待0.1秒
286
+ retryCount++;
287
+ }
288
+ } catch (error) {
289
+ throw new Error("重试机制错误!" + error);
290
+ }
291
+ }
292
+ // 如果达到最大重试次数仍然失败,则抛出异常
293
+ throw new Error("请求失败,重试次数已达到上限!");
294
+ }
295
+
296
+ //当前工作目录
297
+ const workDir = process.cwd();
298
+
299
+ const audioQueue = new Queue({
300
+ //最大并发数量
301
+ queueMaxCount: 10,
302
+ //excel 主键-非必填-无主键时自动生成
303
+ primaryKey: "",
304
+ //excel文件路径-必填
305
+ excelPath: path.join(workDir, "excel", "20240531.xlsx"),
306
+ //当前工作路径-必填
307
+ workDir: workDir,
308
+ //当前可执行任务回调-必填
309
+ onProcess: (row) => {
310
+ //..todo 执行需要压缩上传的文件所有过程。
311
+ return new Promise(async (resolve, reject) => {
312
+ //index文件夹 output/0
313
+ let fileName = row.fileName;
314
+ const anotherId = row.queue_id;
315
+ const rowFileName = row.fileName;
316
+ const title = row.title;
317
+ const keyword = row.keyword;
318
+ const indexFilePath = path.join(workDir, "output", anotherId + "");
319
+
320
+ try {
321
+ logger(
322
+ "**************************" +
323
+ row.fileName +
324
+ "**************************"
325
+ );
326
+ // Excel的列名分别为: fileName title keyword
327
+ // const keywordArr = row.keyword.split(",");
328
+ // const filteredArray = keywordArr.filter((item) => item.trim() !== "");
329
+ // const keyword = filteredArray.join(" ");
330
+
331
+ if (!fileName.includes(".")) {
332
+ fileName = row.fileName + ".mp3";
333
+ }
334
+
335
+ const getPathStartTime = new Date();
336
+ // 源音频文件夹路径
337
+ const originFilePath = await findFileInDir(
338
+ path.join(workDir, ORIGIN_FILE_DIR),
339
+ fileName
340
+ );
341
+
342
+ const getPathEndTime = new Date();
343
+ const timeInSeconds1 = (
344
+ (getPathEndTime - getPathStartTime) /
345
+ 1000
346
+ ).toFixed(2);
347
+
348
+ logger(`第${index}条Path路径搜索时间:${timeInSeconds1}秒`);
349
+ logger(`-------------------${new Date()}------------------`);
350
+
351
+ logger("原文件路径:" + originFilePath);
352
+ logger(`第${index}条原文件路径:${originFilePath}`);
353
+
354
+ if (!originFilePath) {
355
+ logger(`第${index}条音频文件 ${fileName} 不存在`);
356
+ return;
357
+ }
358
+
359
+ await fs_asnyc.access(originFilePath, fs_asnyc.constants.F_OK);
360
+ logFileSize(originFilePath, index);
361
+ const dataParams = await getMetadata(originFilePath, {
362
+ rowFileName,
363
+ title,
364
+ keyword,
365
+ anotherId,
366
+ });
367
+ await wavAudio(indexFilePath, originFilePath, fileName);
368
+ await watermarkAudio(
369
+ indexFilePath,
370
+ originFilePath,
371
+ fileName,
372
+ dataParams.duration
373
+ );
374
+ await archiveZip(fileName, indexFilePath, originFilePath);
375
+
376
+ // 重试机制
377
+ await postDataWithRetry(dataParams, indexFilePath, index);
378
+
379
+ logger(
380
+ `----------------------------------------第${index}条结束---------------------------------end`
381
+ );
382
+ resolve();
383
+ } catch (error) {
384
+ // 可以约定code,来表示不同的错误信息
385
+ if (error.code === "ENOENT") {
386
+ logger(`音频文件 ${fileName} 不存在`);
387
+ } else {
388
+ logger("音频任务失败(最外层catch):" + error);
389
+ }
390
+ removeDirectory(indexFilePath);
391
+
392
+ reject();
393
+ }
394
+ });
395
+ },
396
+ //当前任务全部结束-必填
397
+ onComplete: () => {
398
+ console.log("Congratulations Complete");
399
+ },
400
+ });
401
+
402
+ //开始上传任务
403
+ audioQueue.start();
@@ -41,7 +41,7 @@ const exeDir = __dirname;
41
41
  const workDir = process.cwd();
42
42
 
43
43
  //图片水印
44
- const watermarkImage = path.join(exeDir, "imgs", "miz-watermark.png");
44
+ const watermarkImage = path.join(exeDir, "..", "..", "imgs", "picWater.png");
45
45
 
46
46
  // 表格目录,从excel文件夹中查找
47
47
  const excelFiles = fs
@@ -278,7 +278,7 @@ const postData = (dataParams, indexFilePath, index) => {
278
278
 
279
279
  // return axios.post("http://192.168.102.61:9999/upload/sound", formData, {
280
280
  // return axios.post("http://192.168.101.149:9999/upload/sound", formData, {
281
- return axios.post("http://127.0.0.1:9999/upload/video", formData, {
281
+ return axios.post("http://127.0.0.1:9999/upload/photo", formData, {
282
282
  headers: {
283
283
  "Content-Type": "multipart/form-data",
284
284
  },
@@ -514,7 +514,7 @@ const main = () => {
514
514
  //当前任务hash
515
515
  let hash = "";
516
516
  let jsonData = [];
517
- logger("》》》》》》》》》》视频批量任务任务开始《《《《《《《《《《" + hash);
517
+ logger("》》》》》》》》》》图片批量任务任务开始《《《《《《《《《《" + hash);
518
518
  logger("当前目录: " + workDir + ";工作目录: " + exeDir);
519
519
 
520
520
  // 读取success.txt,判断是否是 异常中断,
package/src/queue.js CHANGED
@@ -49,6 +49,7 @@ class Queue {
49
49
  this.workDir = props.workDir;
50
50
  this.onProcess = props.onProcess;
51
51
  this.onComplete = props.onComplete;
52
+ this.queueMaxCount = props.queueMaxCount;
52
53
 
53
54
  this.init();
54
55
  }
@@ -41,7 +41,13 @@ const exeDir = __dirname;
41
41
  const workDir = process.cwd();
42
42
 
43
43
  //图片水印
44
- const watermarkImage = path.join(exeDir, "imgs", "miz-watermark.png");
44
+ const watermarkImage = path.join(
45
+ exeDir,
46
+ "..",
47
+ "..",
48
+ "imgs",
49
+ "miz-watermark.png"
50
+ );
45
51
 
46
52
  // 表格目录,从excel文件夹中查找
47
53
  const excelFiles = fs
@@ -331,7 +337,7 @@ const archiveZip = (fileName, inputPath, originFilePath) => {
331
337
  const getMetadata = async (
332
338
  indexFilePath,
333
339
  originFilePath,
334
- { title, keyword, anotherId }
340
+ { title, keyword, anotherId, category_id, demand_kind, tag_id }
335
341
  ) => {
336
342
  //第一张截图
337
343
  const photoInputPath = path.join(
@@ -354,11 +360,11 @@ const getMetadata = async (
354
360
  keyword,
355
361
  pr: 0,
356
362
  format: "mp4",
357
- category_id: 4,
358
- demand_kind: 23,
363
+ category_id: category_id,
364
+ demand_kind: demand_kind,
359
365
  source_from: 74,
360
366
  plate_id: 5,
361
- tag_id: 158,
367
+ tag_id: tag_id,
362
368
  is_government: 0,
363
369
  preview_width: "",
364
370
  preview_height: "",
@@ -421,7 +427,7 @@ const postData = (dataParams, indexFilePath, index) => {
421
427
  headers: {
422
428
  "Content-Type": "multipart/form-data",
423
429
  },
424
- timeout: 300000,
430
+ timeout: 1200000,
425
431
  });
426
432
  };
427
433
 
@@ -479,6 +485,10 @@ const task = async (row, index, hash, type) => {
479
485
  const rowFileName = row.fileName;
480
486
  const title = row.title;
481
487
  const keyword = row.keyword;
488
+ const category_id = row.category_id;
489
+ const demand_kind = row.demand_kind;
490
+ const tag_id = row.tag_id;
491
+
482
492
  /**
483
493
  * 唯一标识符id,仅错误excel中有 anotherI d 列
484
494
  */
@@ -494,7 +504,7 @@ const task = async (row, index, hash, type) => {
494
504
  // }
495
505
 
496
506
  const getPathStartTime = new Date();
497
- // 源音频文件夹路径
507
+ // 源视频文件夹路径
498
508
  const originFilePath = await findFileInDir(
499
509
  path.join(workDir, ORIGIN_FILE_DIR),
500
510
  fileName
@@ -512,7 +522,7 @@ const task = async (row, index, hash, type) => {
512
522
  logger(`第${index}条原文件路径:${originFilePath}`);
513
523
 
514
524
  if (!originFilePath) {
515
- logger(`第${index}条音频文件 ${fileName} 不存在`);
525
+ logger(`第${index}条视频文件 ${fileName} 不存在`);
516
526
  return;
517
527
  }
518
528
 
@@ -529,6 +539,9 @@ const task = async (row, index, hash, type) => {
529
539
  title,
530
540
  keyword,
531
541
  anotherId,
542
+ category_id,
543
+ demand_kind,
544
+ tag_id,
532
545
  });
533
546
 
534
547
  // 重试机制
@@ -547,9 +560,9 @@ const task = async (row, index, hash, type) => {
547
560
  } catch (error) {
548
561
  // 可以约定code,来表示不同的错误信息
549
562
  if (error.code === "ENOENT") {
550
- logger(`音频文件 ${fileName} 不存在`);
563
+ logger(`视频文件 ${fileName} 不存在`);
551
564
  } else {
552
- logger("音频任务失败(最外层catch):" + error);
565
+ logger("视频任务失败(最外层catch):" + error);
553
566
  }
554
567
  removeDirectory(indexFilePath);
555
568
  disposeError(hash, { fileName, keyword, title, anotherId });
@@ -576,7 +589,7 @@ const run = (index, data, hash, type) => {
576
589
  const status = runErrorList(taskIndex, hash);
577
590
  if (!status) {
578
591
  logger(
579
- "》》》》》》》》》》音频批量任务任务结束《《《《《《《《《《" + hash
592
+ "》》》》》》》》》》视频批量任务任务结束《《《《《《《《《《" + hash
580
593
  );
581
594
  }
582
595
 
@@ -670,7 +683,7 @@ const main = () => {
670
683
  const status = runErrorList(0, hash);
671
684
  if (!status) {
672
685
  logger(
673
- "》》》》》》》》》》音频批量任务任务结束《《《《《《《《《《" + hash
686
+ "》》》》》》》》》》视频批量任务任务结束《《《《《《《《《《" + hash
674
687
  );
675
688
  }
676
689
  } else {
package/utils/index.js CHANGED
File without changes
package/index.js DELETED
@@ -1,618 +0,0 @@
1
- #!/usr/bin/env node
2
- const XLSX = require("xlsx");
3
- const ffmpeg = require("fluent-ffmpeg");
4
- const fs = require("fs");
5
- const path = require("path");
6
- const fs_asnyc = require("fs").promises;
7
- const archiver = require("archiver");
8
- const axios = require("axios");
9
- const generateUniqueHash = require("./utils/index");
10
- const FormData = require("form-data");
11
-
12
- //执行目录
13
- const exeDir = __dirname;
14
- //当前工作目录
15
- const workDir = process.cwd();
16
-
17
- // 表格目录
18
- const excelDir = path.join(workDir, "excel", "excel.xlsx");
19
- //水印
20
- const watermarkImage = path.join(__dirname, "imgs", "miz-watermark.png");
21
- // 截图数量
22
- const SCREENSHOT_COUNT = 5;
23
- const ORIGIN_FILE_DIR = "video";
24
- const ZIP_VIDEO_DIR = "zip_video";
25
- const ZIP_VIDEO_DIR_400 = "small_video";
26
- const SCREENSHOT_DIR = "screenshots";
27
- const ZIP_WATERMARK_VIDEO_DIR = "zip_watermark_video";
28
- const ZIP_SCREENSHOT_DIR = "screenshot_watermark";
29
- const SOURCE_VIDEO_DIR = "source_video";
30
- const ZIP_FILES_DIR = "zip";
31
-
32
- //并发数量
33
- const queueCount = 50;
34
-
35
- //起始任务下标
36
- let taskIndex = 2066;
37
-
38
- /**
39
- * 递归遍历文件夹,查找mp4文件
40
- * @param {*} dir 文件夹路径
41
- * @param {*} fileName 文件名称
42
- * @returns string 匹配成功的文件路径
43
- */
44
- function findFileInDir(dir, fileName) {
45
- const files = fs.readdirSync(dir);
46
- for (const file of files) {
47
- const filePath = path.join(dir, file);
48
- const stat = fs.statSync(filePath);
49
- if (stat.isDirectory()) {
50
- const result = findFileInDir(filePath, fileName);
51
- if (result) {
52
- return result;
53
- }
54
- } else if (
55
- file.startsWith(fileName) &&
56
- (file.endsWith(".mp4") || file.endsWith(".mov"))
57
- ) {
58
- return filePath;
59
- }
60
- }
61
- return null;
62
- }
63
-
64
- /**
65
- * 确保目录存在,如果不存在则创建它
66
- */
67
- function ensureDirSync(dirpath) {
68
- try {
69
- if (!fs.existsSync(dirpath)) {
70
- fs.mkdirSync(dirpath, { recursive: true });
71
- logger(`目录创建成功:${dirpath}`);
72
- }
73
- } catch (err) {
74
- logger(`创建目录时出错:${err}`);
75
- throw err;
76
- }
77
- }
78
-
79
- /**
80
- * 检查文件是否存在
81
- */
82
- function checkFileExistence(path) {
83
- return new Promise((resolve, reject) => {
84
- fs.access(path, fs.constants.F_OK, (err) => {
85
- if (!err) {
86
- logger("文件存在,可以访问");
87
- resolve(true);
88
- } else {
89
- logger(`视频文件 ${path} 不存在`);
90
- resolve(false);
91
- }
92
- });
93
- });
94
- }
95
-
96
- //普通日志
97
- const logger = (log, hash, err) => {
98
- console.log(log);
99
- fs.writeFileSync("log.txt", log + "\n", {
100
- flag: "a",
101
- });
102
- };
103
-
104
- //错误日志
105
- const disposeError = (fileName) => {
106
- logger("---任务失败---" + fileName);
107
- logger(
108
- `******************************************************************end`
109
- );
110
- fs.writeFileSync("error.txt", fileName + "\n", {
111
- flag: "a",
112
- });
113
- };
114
-
115
- const readExcel = (path) => {
116
- const workbook = XLSX.readFile(path);
117
-
118
- // 获取第一个工作表(Sheet)
119
- const firstSheetName = workbook.SheetNames[0];
120
- const worksheet = workbook.Sheets[firstSheetName];
121
-
122
- // 将工作表转换为 JSON 对象
123
- const jsonData = XLSX.utils.sheet_to_json(worksheet);
124
- return jsonData;
125
- };
126
-
127
- function logFileSize(path) {
128
- const fileSize = fs.statSync(path).size / (1024 * 1024);
129
- const formattedSize = fileSize.toFixed(2); // 保留两位小数
130
- logger(`视频大小:${formattedSize}M`);
131
- }
132
-
133
- /**
134
- * 压缩视频
135
- * @param {*} fileName 文件名
136
- * @param {*} outputFileDir 产物文件夹
137
- * @param {*} inputFilePath 源文件路径
138
- * @returns Promise
139
- */
140
- const compressVideo = (fileName, outputFileDir, inputFilePath) => {
141
- // const inputFilePath = path.join(workDir, "video", fileName);
142
- const outputFilePath = path.join(
143
- outputFileDir,
144
- ZIP_VIDEO_DIR,
145
- `zip-${fileName}`
146
- );
147
- const comand = ffmpeg(inputFilePath);
148
-
149
- ensureDirSync(path.dirname(outputFilePath));
150
- return new Promise((resolve, reject) => {
151
- comand
152
- .videoCodec("libx264")
153
- .size("1280x?")
154
- // .size("1280x720")
155
-
156
- .output(outputFilePath)
157
-
158
- .on("start", () => {
159
- logger("视频开始压缩……");
160
- })
161
- .on("end", () => {
162
- logger("视频压缩完成!");
163
- resolve(outputFilePath);
164
- })
165
- .on("error", (err) => {
166
- logger("视频压缩出错:" + err);
167
- reject(err);
168
- })
169
- .run();
170
- });
171
- };
172
-
173
- /**
174
- * 压缩400p视频
175
- */
176
- const compressVideo400p = (fileName, outputFileDir, inputFilePath) => {
177
- const outputFilePath = path.join(
178
- outputFileDir,
179
- ZIP_VIDEO_DIR_400,
180
- `zip-${fileName}`
181
- );
182
- const comand = ffmpeg(inputFilePath);
183
-
184
- ensureDirSync(path.dirname(outputFilePath));
185
- return new Promise((resolve, reject) => {
186
- comand
187
- .seekInput(0)
188
- .inputOptions("-t 10")
189
- .videoCodec("libx264")
190
- .size("400x?")
191
-
192
- .output(outputFilePath)
193
-
194
- .on("start", () => {
195
- logger("400p视频开始压缩……");
196
- })
197
- .on("end", () => {
198
- logger("400p视频压缩完成!");
199
- resolve(outputFilePath);
200
- })
201
- .on("error", (err) => {
202
- logger("视频压缩出错:", err);
203
- reject(err);
204
- })
205
- .run();
206
- });
207
- };
208
-
209
- /**
210
- * 生成5张截图
211
- */
212
- const get5Screenshots = (fileName, outputFilePath, inputFilePath) => {
213
- const folderPath = path.join(outputFilePath, SCREENSHOT_DIR); // 构建完整的目录路径
214
-
215
- ensureDirSync(folderPath);
216
- return new Promise((resolve, reject) => {
217
- const screenshotsCommand = ffmpeg(inputFilePath);
218
-
219
- screenshotsCommand
220
- .on("start", () => {
221
- logger("开始从视频中截图……");
222
- })
223
- .on("end", (stdout, stderr) => {
224
- logger(inputFilePath + "截图完成!!!");
225
- resolve();
226
- })
227
- .on("error", (err) => {
228
- logger(inputFilePath + "截图出错:", err);
229
- reject(err);
230
- })
231
-
232
- .screenshots({
233
- timemarks: [0, "25%", "50%", "75%", "90%"],
234
- folder: folderPath,
235
- filename: `screenshots-%i.png`,
236
- size: "840x?",
237
- });
238
- });
239
- };
240
-
241
- /**
242
- * 视频打水印
243
- */
244
- const watermarkVideo = (fileName, outputFileDir) => {
245
- //用压缩后的视频作为输入源,增加效率
246
- const inputFilePath = path.join(
247
- outputFileDir,
248
- ZIP_VIDEO_DIR,
249
- `zip-${fileName}`
250
- );
251
- const outputFilePath = path.join(
252
- outputFileDir,
253
- ZIP_WATERMARK_VIDEO_DIR,
254
- `zip-watermark-${fileName}`
255
- );
256
- const watermarkCommand = ffmpeg(inputFilePath);
257
-
258
- ensureDirSync(path.dirname(outputFilePath));
259
-
260
- return new Promise((resolve, reject) => {
261
- watermarkCommand
262
- .input(watermarkImage) // 添加水印图片作为第二个输入
263
-
264
- .complexFilter([
265
- "[0:v][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2",
266
- ])
267
- .output(outputFilePath)
268
- .on("start", () => {
269
- logger("视频开始打水印……");
270
- })
271
- .on("end", () => {
272
- logger("视频打水印完成!");
273
- resolve(outputFilePath);
274
- })
275
- .on("error", (err) => {
276
- logger("视频打水印出错:", err);
277
- reject(err);
278
- })
279
- .run();
280
- });
281
- };
282
-
283
- /**
284
- * 截图水印,根据截图打水印
285
- */
286
- const watermarkScreenshots = (indexFilePath) => {
287
- //存放水印截图的文件夹
288
- const inputDir = path.join(indexFilePath, SCREENSHOT_DIR);
289
- const outputDir = path.join(indexFilePath, ZIP_SCREENSHOT_DIR);
290
- ensureDirSync(outputDir);
291
-
292
- return new Promise((resolve, reject) => {
293
- fs.readdir(inputDir, (err, files) => {
294
- if (err) {
295
- logger("读取截图文件夹出错: " + err);
296
- return;
297
- } else {
298
- files.forEach((file, index) => {
299
- if (path.extname(file) === ".png") {
300
- const inputFile = path.join(inputDir, file);
301
- const outputFile = path.join(outputDir, file);
302
-
303
- const watermarkScreenshotsComand = ffmpeg(inputFile);
304
- watermarkScreenshotsComand
305
- .input(watermarkImage)
306
- .complexFilter([
307
- "[0:v][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2",
308
- ])
309
- .output(outputFile)
310
- .on("error", (err) => {
311
- logger("截图添加水印出错: ", err);
312
- reject();
313
- })
314
- .on("end", () => {
315
- logger("截图水印添加完成: " + outputFile);
316
- if (index + 1 === SCREENSHOT_COUNT) {
317
- resolve();
318
- }
319
- })
320
- .run();
321
- } else {
322
- logger("截图不是png文件,无法添加图片水印!");
323
- reject();
324
- }
325
- });
326
- }
327
- });
328
- });
329
- };
330
-
331
- /**
332
- * 打包物料
333
- */
334
- const archiveZip = (fileName, inputPath, originFilePath) => {
335
- const zipDir = path.join(inputPath, "zip");
336
- const timestamp = new Date().getTime();
337
-
338
- ensureDirSync(zipDir);
339
- // create a file to stream archive data to.
340
- const zipStream = fs.createWriteStream(
341
- path.join(zipDir, `package${timestamp}.zip`)
342
- );
343
- const archive = archiver("zip", {
344
- zlib: { level: 9 }, // Sets the compression level.
345
- });
346
-
347
- return new Promise((resolve, reject) => {
348
- // listen for all archive data to be written
349
- // 'close' event is fired only when a file descriptor is involved
350
- zipStream.on("close", function () {
351
- logger("压缩数据:" + archive.pointer() + " total bytes");
352
- logger(
353
- "完成归档archiver has been finalized and the output file descriptor has closed."
354
- );
355
- resolve();
356
- });
357
-
358
- // good practice to catch warnings (ie stat failures and other non-blocking errors)
359
- archive.on("warning", function (err) {
360
- if (err.code === "ENOENT") {
361
- logger("压缩-warning:" + err);
362
- } else {
363
- // throw error
364
- throw err;
365
- }
366
- });
367
-
368
- // good practice to catch this error explicitly
369
- archive.on("error", function (err) {
370
- logger("压缩失败!" + err);
371
- reject();
372
- });
373
-
374
- archive.pipe(zipStream);
375
-
376
- const directories = [
377
- ZIP_VIDEO_DIR,
378
- SCREENSHOT_DIR,
379
- ZIP_WATERMARK_VIDEO_DIR,
380
- ZIP_SCREENSHOT_DIR,
381
- ZIP_VIDEO_DIR_400,
382
- ];
383
-
384
- directories.forEach((dir) => {
385
- const dirPath = path.join(inputPath, dir);
386
- archive.directory(dirPath, dir);
387
- });
388
-
389
- archive.file(originFilePath, {
390
- name: path.join(SOURCE_VIDEO_DIR, fileName),
391
- });
392
- // 完成归档
393
- archive.finalize();
394
- });
395
- };
396
-
397
- /**
398
- * 获取 元数据
399
- */
400
- const getMetadata = async (
401
- fileName,
402
- indexFilePath,
403
- originFilePath,
404
- { rowFileName, title, keyword }
405
- ) => {
406
- //第一张截图
407
- const photoInputPath = path.join(
408
- indexFilePath,
409
- SCREENSHOT_DIR,
410
- "screenshots-1.png"
411
- );
412
-
413
- const videoMetadataComand = ffmpeg(originFilePath);
414
- const photoMetadataComand = ffmpeg(photoInputPath);
415
- const metaDataParams = {
416
- userid: 1003,
417
- username: "美好景象",
418
- pixel_width: "",
419
- pixel_height: "",
420
- size: "",
421
- duration: "",
422
- video_id: rowFileName,
423
- title,
424
- keyword,
425
- pr: 100,
426
- format: "mp4",
427
- category_id: 4,
428
- demand_kind: 23,
429
- source_from: 71,
430
- plate_id: 5,
431
- tag_id: 158,
432
- is_government: 0,
433
- preview_width: "",
434
- preview_height: "",
435
- };
436
- await new Promise((resolve, reject) => {
437
- videoMetadataComand.ffprobe(function (err, metadata) {
438
- const videoStream = metadata.streams.find(
439
- (s) => s.codec_type === "video"
440
- );
441
- const formatStream = metadata.format;
442
-
443
- metaDataParams.pixel_width = videoStream.width;
444
- metaDataParams.pixel_height = videoStream.height;
445
- metaDataParams.duration = videoStream.duration;
446
- metaDataParams.size = formatStream.size;
447
- resolve();
448
- });
449
- });
450
-
451
- await new Promise((resolve, reject) => {
452
- photoMetadataComand.ffprobe(function (err, metadata) {
453
- const photoStream = metadata.streams.find(
454
- (s) => s.codec_type === "video"
455
- );
456
-
457
- metaDataParams.preview_width = photoStream.width;
458
- metaDataParams.preview_height = photoStream.height;
459
- resolve();
460
- });
461
- });
462
-
463
- return metaDataParams;
464
- };
465
-
466
- /**
467
- * 接口
468
- */
469
- const postData = (dataParams, indexFilePath) => {
470
- const formData = new FormData();
471
- //风险:确保文件夹中只要一个zip文件,(因为时间戳,老的文件不会被覆盖)
472
- const zipFiles = fs
473
- .readdirSync(path.join(indexFilePath, ZIP_FILES_DIR))
474
- .find((file) => file.endsWith(".zip"));
475
-
476
- const packageZip = path.join(indexFilePath, ZIP_FILES_DIR, zipFiles);
477
-
478
- formData.append("file", fs.createReadStream(packageZip));
479
- for (const key in dataParams) {
480
- if (Object.hasOwnProperty.call(dataParams, key)) {
481
- const value = dataParams[key];
482
- formData.append(key, value);
483
- }
484
- }
485
-
486
- logger("等待接口返回结果……");
487
-
488
- return axios.post("http://127.0.0.1:9999/upload/video", formData, {
489
- headers: {
490
- "Content-Type": "multipart/form-data",
491
- },
492
- timeout: 600000,
493
- });
494
- };
495
-
496
- /**
497
- * 任务
498
- */
499
- const task = async (row, index) => {
500
- logger(
501
- "**************************" + row.fileName + "**************************"
502
- );
503
- // Excel的列名分别为: fileName title keyword
504
- // 表格中文件名无后缀,遂手动添加写死为mp4
505
- let fileName = row.fileName;
506
- const rowFileName = row.fileName;
507
- const title = row.title;
508
- const keywordArr = row.keyword.split(",");
509
- const filteredArray = keywordArr.filter((item) => item.trim() !== "");
510
- const keyword = filteredArray.join(" ");
511
-
512
- if (!fileName.includes(".")) {
513
- fileName = row.fileName + ".mov";
514
- }
515
-
516
- // 源视频文件路径
517
- const originFilePath = findFileInDir(
518
- path.join(workDir, ORIGIN_FILE_DIR),
519
- fileName
520
- );
521
-
522
- if (!originFilePath) {
523
- logger(`视频文件 ${fileName} 不存在`);
524
- return;
525
- }
526
- logger("源视频文件路径");
527
- logger(originFilePath);
528
-
529
- //index文件夹 output/0
530
- const indexFilePath = path.join(workDir, "output", index + "");
531
-
532
- try {
533
- await fs_asnyc.access(originFilePath, fs_asnyc.constants.F_OK);
534
- logFileSize(originFilePath);
535
- await compressVideo(fileName, indexFilePath, originFilePath);
536
- await compressVideo400p(fileName, indexFilePath, originFilePath);
537
- await get5Screenshots(fileName, indexFilePath, originFilePath);
538
- await watermarkVideo(fileName, indexFilePath);
539
- await watermarkScreenshots(indexFilePath);
540
- await archiveZip(fileName, indexFilePath, originFilePath);
541
- const dataParams = await getMetadata(
542
- fileName,
543
- indexFilePath,
544
- originFilePath,
545
- {
546
- rowFileName,
547
- title,
548
- keyword,
549
- }
550
- );
551
-
552
- const resData = await postData(dataParams, indexFilePath);
553
- if (resData.data.code === 200) {
554
- logger("请求成功!");
555
- logger(resData.data.code);
556
- logger(resData.data);
557
- //删除文件夹
558
- fs.rmdirSync(indexFilePath, { recursive: true });
559
- } else {
560
- throw new Error("请求失败!" + resData.data.msg);
561
- }
562
- logger(resData.data);
563
- logger(
564
- `----------------------------------------第${index}条结束---------------------------------end`
565
- );
566
- } catch (error) {
567
- // 可以约定code,来表示不同的错误信息
568
- if (error.code === "ENOENT") {
569
- logger(`视频文件 ${fileName} 不存在`);
570
- } else {
571
- logger("视频处理失败:");
572
- logger(error);
573
- }
574
- logger("catch日志*********************************");
575
- disposeError(fileName);
576
- fs.rmdirSync(indexFilePath, { recursive: true });
577
- }
578
- };
579
-
580
- /**
581
- * 解析Excel数据
582
- */
583
- const transitionExcelToJSON = () => {
584
- //当前任务hash
585
- const hashOrigin = generateUniqueHash();
586
- const hash = hashOrigin.slice(0, 8);
587
- logger("任务开始---" + hash);
588
- logger("当前目录: " + workDir);
589
- logger("工作目录: " + __dirname);
590
-
591
- const jsonData = readExcel(excelDir);
592
-
593
- const run = (index) => {
594
- const row = jsonData[index];
595
- if (!row) {
596
- return;
597
- }
598
- logger(
599
- `----------------------------------------第${index}条开始------------------------------------`
600
- );
601
- logger(`-------------------${new Date()}------------------`);
602
- task(row, index)
603
- .then(() => {
604
- taskIndex++;
605
- run(taskIndex);
606
- })
607
- .catch(() => {
608
- taskIndex++;
609
- run(taskIndex);
610
- });
611
- };
612
-
613
- for (let i = 0; i < queueCount; i++) {
614
- run(i + taskIndex);
615
- }
616
- };
617
-
618
- transitionExcelToJSON();