tools_batch_files 1.0.28 → 1.0.30

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/index.js CHANGED
@@ -1,618 +1,618 @@
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();
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();