tools_batch_files 1.0.36 → 1.0.38

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.36",
3
+ "version": "1.0.38",
4
4
  "description": "批处理视频工具",
5
5
  "keywords": [
6
6
  "utils",
@@ -12,9 +12,9 @@
12
12
  "bin": {
13
13
  "tbf": "./index.js",
14
14
  "tbfp": "./src/photoBatch.js",
15
+ "tbfps": "./src/photoFn/photoBatch.js",
15
16
  "tbfa": "./src/audioFn/audioBatch.js",
16
17
  "tbfv": "./src/videoFn/videoBatch.js",
17
- "tbfb": "./src/photoBatchBack.js",
18
18
  "tbfre": "./src/removeFailVideo.js"
19
19
  },
20
20
  "scripts": {
@@ -30,6 +30,7 @@
30
30
  "axios": "^1.6.8",
31
31
  "ffmpeg": "^0.0.4",
32
32
  "fluent-ffmpeg": "^2.1.2",
33
+ "ini": "^4.1.3",
33
34
  "xlsx": "^0.18.5"
34
35
  },
35
36
  "files": [
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 模版文件-演示如何使用队列
5
+ */
6
+
7
+ // import { Queue } from "./queue";
8
+ const { Queue } = require("./queue");
9
+ const path = require("path");
10
+
11
+ //当前工作目录
12
+ const workDir = process.cwd();
13
+
14
+ const templateQueue = new Queue({
15
+ //excel 主键-非必填-无主键时自动生成
16
+ primaryKey: "",
17
+ //excel文件路径-必填
18
+ excelPath: path.join(workDir, "excel", "20240531.xlsx"),
19
+ //当前工作路径-必填
20
+ workDir: workDir,
21
+ //当前可执行任务回调-必填
22
+ onProcess: (data) => {
23
+ //..todo 执行需要压缩上传的文件所有过程。
24
+ return new Promise((resolve, rejects) => {
25
+ const random = Math.random() * 100;
26
+ if (random < 20) {
27
+ console.log("onProcess Success", data.queue_id);
28
+ //上传成功
29
+ resolve();
30
+ } else {
31
+ console.log("onProcess error", data.queue_id);
32
+ //上传失败
33
+ rejects();
34
+ }
35
+ });
36
+ },
37
+ //当前任务全部结束-必填
38
+ onComplete: () => {
39
+ console.log("Congratulations Complete");
40
+ },
41
+ });
42
+
43
+ //开始上传任务
44
+ templateQueue.start();
@@ -0,0 +1,560 @@
1
+ #!/usr/bin/env node
2
+ const generateUniqueHash = require("../../utils/index");
3
+ const {
4
+ disposeError,
5
+ readExcel,
6
+ logger,
7
+ successLogger,
8
+ } = require("../../utils/logger");
9
+
10
+ const {
11
+ findFileInDir,
12
+ logFileSize,
13
+ ensureDirSync,
14
+ isExist,
15
+ readTxt,
16
+ removeDirectory,
17
+ } = require("../../utils/settleFiles");
18
+ const ffmpeg = require("fluent-ffmpeg");
19
+ const fs = require("fs");
20
+ const path = require("path");
21
+ const fs_asnyc = require("fs").promises;
22
+ const archiver = require("archiver");
23
+ const axios = require("axios");
24
+ const FormData = require("form-data");
25
+
26
+ //并发数量
27
+ const queueCount = 10;
28
+
29
+ //起始任务下标
30
+ let taskIndex = 0;
31
+
32
+ //已完成数量
33
+ let completeCount = 0;
34
+
35
+ //错误文件重试次数
36
+ let retryError = 0;
37
+
38
+ //执行目录
39
+ const exeDir = __dirname;
40
+ //当前工作目录
41
+ const workDir = process.cwd();
42
+
43
+ //图片水印
44
+ const watermarkImage = path.join(exeDir, "imgs", "miz-watermark.png");
45
+
46
+ // 表格目录,从excel文件夹中查找
47
+ const excelFiles = fs
48
+ .readdirSync(path.join(workDir, "excel"))
49
+ .find((file) => file.endsWith(".xlsx"));
50
+ const excelDir = path.join(workDir, "excel", excelFiles);
51
+
52
+ const ORIGIN_FILE_DIR = "files";
53
+ const ZIP_FILES_DIR = "zip";
54
+ const SCREENSHOT_DIR = "preview_photo";
55
+ const SOURCE_PHOTO_DIR = "source_photo";
56
+ const ZIP_SCREENSHOT_WM_DIR = "preview_watermark_photo";
57
+
58
+ const maxRetries = 1; // 最大重试次数
59
+
60
+ /**
61
+ * 生成预览图 indexFilePath: output/0
62
+ * originFilePath E:\workspace\图片测试\photos\abc222.png
63
+ */
64
+ const PriviewPhoto = (indexFilePath, originFilePath, fileName) => {
65
+ //存放预览图图的文件夹
66
+ const outputDir = path.join(indexFilePath, SCREENSHOT_DIR);
67
+ ensureDirSync(outputDir);
68
+
69
+ const outputFile = path.join(outputDir, fileName);
70
+
71
+ return new Promise((resolve, reject) => {
72
+ const watermarkScreenshotsComand = ffmpeg(originFilePath);
73
+ watermarkScreenshotsComand
74
+ .size("1280x?")
75
+ .output(outputFile)
76
+ .on("error", (err) => {
77
+ logger("截图预览图出错: " + err);
78
+ reject();
79
+ })
80
+ .on("end", () => {
81
+ logger("预览图完成: " + outputFile);
82
+ resolve();
83
+ })
84
+ .run();
85
+ });
86
+ };
87
+
88
+ /**
89
+ * 生成水印预览图
90
+ */
91
+ const watermarkPriviewPhoto = (indexFilePath, originFilePath, fileName) => {
92
+ //存放水印截图的文件夹
93
+ const outputDir = path.join(indexFilePath, ZIP_SCREENSHOT_WM_DIR);
94
+ const inputDir = path.join(indexFilePath, SCREENSHOT_DIR, fileName);
95
+ ensureDirSync(outputDir);
96
+
97
+ const outputFile = path.join(outputDir, fileName);
98
+
99
+ return new Promise((resolve, reject) => {
100
+ const watermarkScreenshotsComand = ffmpeg(inputDir);
101
+ watermarkScreenshotsComand
102
+ .input(watermarkImage)
103
+ .complexFilter("overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2")
104
+ .output(outputFile)
105
+ .on("error", (err) => {
106
+ logger("截图添加水印出错: " + err);
107
+ reject();
108
+ })
109
+ .on("end", () => {
110
+ logger("截图水印添加完成: " + outputFile);
111
+ resolve();
112
+ })
113
+ .run();
114
+ });
115
+ };
116
+
117
+ /**
118
+ * 打包物料
119
+ */
120
+ const archiveZip = (fileName, inputPath, originFilePath) => {
121
+ const zipDir = path.join(inputPath, "zip");
122
+ const timestamp = new Date().getTime();
123
+
124
+ ensureDirSync(zipDir);
125
+ const zipStream = fs.createWriteStream(
126
+ path.join(zipDir, `package${timestamp}.zip`)
127
+ );
128
+ const archive = archiver("zip", {
129
+ zlib: { level: 9 },
130
+ });
131
+
132
+ return new Promise((resolve, reject) => {
133
+ zipStream.on("close", function () {
134
+ logger("压缩数据:" + archive.pointer() + " total bytes");
135
+ logger(
136
+ "完成归档archiver has been finalized and the output file descriptor has closed."
137
+ );
138
+ resolve();
139
+ });
140
+
141
+ archive.on("warning", function (err) {
142
+ if (err.code === "ENOENT") {
143
+ logger("压缩-warning:" + err);
144
+ } else {
145
+ throw err;
146
+ }
147
+ });
148
+
149
+ archive.on("error", function (err) {
150
+ logger("压缩失败!" + err);
151
+ reject();
152
+ });
153
+
154
+ archive.pipe(zipStream);
155
+ const directories = [ZIP_SCREENSHOT_WM_DIR, SCREENSHOT_DIR];
156
+
157
+ directories.forEach((dir) => {
158
+ const dirPath = path.join(inputPath, dir);
159
+ archive.directory(dirPath, dir);
160
+ });
161
+
162
+ archive.file(originFilePath, {
163
+ name: path.join(SOURCE_PHOTO_DIR, fileName),
164
+ });
165
+ // 完成归档
166
+ archive.finalize();
167
+ });
168
+ };
169
+
170
+ /**
171
+ * 获取 元数据
172
+ */
173
+ const getMetadata = async (
174
+ fileName,
175
+ indexFilePath,
176
+ originFilePath,
177
+ { anotherId, title, keyword }
178
+ ) => {
179
+ //源图片数据
180
+ const photoMetadataComand = ffmpeg(originFilePath);
181
+ //预览图数据
182
+ const photoInputPath = path.join(indexFilePath, SCREENSHOT_DIR, fileName);
183
+ const photoPreviewMetadataComand = ffmpeg(photoInputPath);
184
+
185
+ const metaDataParams = {
186
+ photo_id: anotherId,
187
+ width: "",
188
+ height: "",
189
+ format: "",
190
+ size: "",
191
+ pr: 0,
192
+ dpi: 0,
193
+ is_government: 0,
194
+ common_width: "",
195
+ common_height: "",
196
+ dujia: 0,
197
+ xiaoxiang: 0,
198
+ title,
199
+ keyword,
200
+ en_keyword: "",
201
+ // preview_path: "",
202
+ // markImage_path: "",
203
+ // photo_path: "",
204
+ // common_image_path: "",
205
+ // en_keyword: "",
206
+ plate_id: 3,
207
+ transform_plate_id: 3,
208
+ tag_id: 47,
209
+ userid: 10876358,
210
+ username: "樊健",
211
+ demand_kind: 184,
212
+ source_from: 74,
213
+ file: "",
214
+ };
215
+
216
+ await new Promise((resolve, reject) => {
217
+ photoMetadataComand.ffprobe(function (err, metadata) {
218
+ // console.log("metadata");
219
+ // console.log(JSON.stringify(metadata));
220
+ if (metadata) {
221
+ const formatStream = metadata.format;
222
+ const photoStream = metadata.streams.find(
223
+ (s) => s.codec_type === "video"
224
+ );
225
+
226
+ metaDataParams.width = photoStream.width;
227
+ metaDataParams.height = photoStream.height;
228
+ // metaDataParams.format = formatStream.format_name;
229
+ metaDataParams.format = "jpg";
230
+ metaDataParams.size = formatStream.size;
231
+
232
+ resolve();
233
+ } else {
234
+ reject(err);
235
+ }
236
+ });
237
+ });
238
+ await new Promise((resolve, reject) => {
239
+ photoPreviewMetadataComand.ffprobe(function (err, metadata) {
240
+ if (metadata) {
241
+ const photoStream = metadata.streams.find(
242
+ (s) => s.codec_type === "video"
243
+ );
244
+
245
+ metaDataParams.common_width = photoStream.width;
246
+ metaDataParams.common_height = photoStream.height;
247
+ resolve();
248
+ } else {
249
+ reject(err);
250
+ }
251
+ });
252
+ });
253
+
254
+ return metaDataParams;
255
+ };
256
+
257
+ /**
258
+ * 接口
259
+ */
260
+ const postData = (dataParams, indexFilePath, index) => {
261
+ const formData = new FormData();
262
+
263
+ const zipFiles = fs
264
+ .readdirSync(path.join(indexFilePath, ZIP_FILES_DIR))
265
+ .find((file) => file.endsWith(".zip"));
266
+
267
+ const packageZip = path.join(indexFilePath, ZIP_FILES_DIR, zipFiles);
268
+
269
+ formData.append("file", fs.createReadStream(packageZip));
270
+ for (const key in dataParams) {
271
+ if (Object.hasOwnProperty.call(dataParams, key)) {
272
+ const value = dataParams[key];
273
+ formData.append(key, value);
274
+ }
275
+ }
276
+
277
+ logger(`第${index}条等待接口返回结果……`);
278
+
279
+ // return axios.post("http://192.168.102.61:9999/upload/sound", formData, {
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, {
282
+ headers: {
283
+ "Content-Type": "multipart/form-data",
284
+ },
285
+ timeout: 300000,
286
+ });
287
+ };
288
+
289
+ /**
290
+ * 接口重试机制
291
+ */
292
+ async function postDataWithRetry(
293
+ dataParams,
294
+ indexFilePath,
295
+ index,
296
+ fileName,
297
+ hash,
298
+ type
299
+ ) {
300
+ let retryCount = 0; // 当前重试次数
301
+
302
+ while (retryCount < maxRetries) {
303
+ try {
304
+ const resData = await postData(dataParams, indexFilePath, index);
305
+ if (resData.data.code === 200) {
306
+ logger("请求成功!");
307
+ logger(resData.data.code);
308
+ // 文件名和索引值
309
+ successLogger(hash, index, fileName, type);
310
+ removeDirectory(indexFilePath);
311
+ return;
312
+ } else if (resData.data.code === 300) {
313
+ // 重复上传,不捕获此错误
314
+ logger(resData.data.code);
315
+ logger(`第${index}条文件-${index}重复上传!`);
316
+ removeDirectory(indexFilePath);
317
+ return;
318
+ } else {
319
+ logger(`请求失败,重试中... (${retryCount + 1}/${maxRetries})`);
320
+ logger(`请求code!==200: ${resData.data.code}${resData.data.msg}`);
321
+ // 延时等待一段时间后再进行重试
322
+ await new Promise((resolve) => setTimeout(resolve, 100)); // 等待0.1秒
323
+ retryCount++;
324
+ }
325
+ } catch (error) {
326
+ throw new Error("重试机制错误!" + error);
327
+ }
328
+ }
329
+ // 如果达到最大重试次数仍然失败,则抛出异常
330
+ throw new Error("请求失败,重试次数已达到上限!");
331
+ }
332
+
333
+ /**
334
+ * 任务
335
+ */
336
+ const task = async (row, index, hash, type) => {
337
+ //index文件夹 output/0
338
+ const indexFilePath = path.join(workDir, "output", index + "");
339
+ let fileName = row.fileName;
340
+ const rowFileName = row.fileName;
341
+ const title = row.title;
342
+ const keyword = row.keyword;
343
+ /**
344
+ * 唯一标识符id,仅错误excel中有 anotherI d 列
345
+ */
346
+ const anotherId = row.anotherId ? row.anotherId : row.fileName;
347
+
348
+ try {
349
+ logger(
350
+ "**************************" + row.fileName + "**************************"
351
+ );
352
+
353
+ // if (!fileName.includes(".")) {
354
+ // fileName = row.fileName + ".mp44444"; //findFileInDir后期返回两个 文件路径和 文件名
355
+ // }
356
+
357
+ const getPathStartTime = new Date();
358
+ // 源图片文件夹路径
359
+ const originFilePath = await findFileInDir(
360
+ path.join(workDir, ORIGIN_FILE_DIR),
361
+ fileName
362
+ );
363
+
364
+ const getPathEndTime = new Date();
365
+ const timeInSeconds1 = ((getPathEndTime - getPathStartTime) / 1000).toFixed(
366
+ 2
367
+ );
368
+
369
+ logger(`第${index}条Path路径搜索时间:${timeInSeconds1}秒`);
370
+ logger(`-------------------${new Date()}------------------`);
371
+
372
+ logger("原文件路径:" + originFilePath);
373
+ logger(`第${index}条原文件路径:${originFilePath}`);
374
+
375
+ if (!originFilePath) {
376
+ logger(`第${index}条图片文件 ${fileName} 不存在`);
377
+ return;
378
+ }
379
+
380
+ await fs_asnyc.access(originFilePath, fs_asnyc.constants.F_OK);
381
+ logFileSize(originFilePath, index);
382
+ await PriviewPhoto(indexFilePath, originFilePath, fileName);
383
+ await watermarkPriviewPhoto(indexFilePath, originFilePath, fileName);
384
+
385
+ await archiveZip(fileName, indexFilePath, originFilePath);
386
+
387
+ const dataParams = await getMetadata(
388
+ fileName,
389
+ indexFilePath,
390
+ originFilePath,
391
+ {
392
+ anotherId,
393
+ title,
394
+ keyword,
395
+ }
396
+ );
397
+
398
+ // 重试机制
399
+ await postDataWithRetry(
400
+ dataParams,
401
+ indexFilePath,
402
+ index,
403
+ fileName,
404
+ hash,
405
+ type
406
+ );
407
+
408
+ logger(
409
+ `----------------------------------------第${index}条结束---------------------------------end`
410
+ );
411
+ } catch (error) {
412
+ // 可以约定code,来表示不同的错误信息
413
+ if (error.code === "ENOENT") {
414
+ logger(`图片文件 ${fileName} 不存在`);
415
+ } else {
416
+ logger("图片任务失败(最外层catch):" + error);
417
+ }
418
+ removeDirectory(indexFilePath);
419
+ disposeError(hash, { fileName, keyword, title, anotherId });
420
+ }
421
+ };
422
+
423
+ const run = (index, data, hash, type) => {
424
+ try {
425
+ logger(
426
+ `run-------------------------------------第${index}条开始------------------------------------`
427
+ );
428
+ const row = data[index];
429
+
430
+ if (completeCount >= data.length) {
431
+ taskIndex = 0;
432
+ logger(
433
+ index +
434
+ "=========================当前任务,最后一条已结束!========================="
435
+ );
436
+
437
+ successLogger(hash, index, "done");
438
+
439
+ // 是否存在错误列表? 存在的话,继续遍历
440
+ const status = runErrorList(taskIndex, hash);
441
+ if (!status) {
442
+ logger(
443
+ "》》》》》》》》》》图片批量任务任务结束《《《《《《《《《《" + hash
444
+ );
445
+ }
446
+
447
+ return;
448
+ }
449
+
450
+ if (!row) {
451
+ return;
452
+ }
453
+
454
+ task(row, index, hash, type)
455
+ .then(() => {
456
+ taskIndex++;
457
+ completeCount++;
458
+ run(taskIndex, data, hash);
459
+ })
460
+ .catch((err) => {
461
+ throw new Error("task失败:" + err);
462
+ });
463
+ } catch (error) {
464
+ logger("捕获错误!" + error);
465
+ completeCount++;
466
+ taskIndex++;
467
+ run(taskIndex, data, hash);
468
+ }
469
+ };
470
+
471
+ const queue = (index, jsonData, hash, type) => {
472
+ const rest = jsonData.length - index;
473
+ const count = queueCount > rest ? rest : queueCount;
474
+ const queueList = [];
475
+ completeCount = index;
476
+
477
+ for (let i = 0; i < count; i++) {
478
+ queueList.push(run(i + index, jsonData, hash, type));
479
+ }
480
+
481
+ taskIndex += count - 1;
482
+ return Promise.all(queueList);
483
+ };
484
+
485
+ //yunxing cuowu wenjian
486
+ const runErrorList = (index, hash, parentHash) => {
487
+ const fileHash = parentHash || hash;
488
+ const errorExcelPath = path.join(workDir, `error${fileHash}.xlsx`);
489
+
490
+ if (retryError > 5) {
491
+ console.log("出现无法解析的错误");
492
+ return false;
493
+ }
494
+
495
+ taskIndex = index;
496
+ let taskHash = hash;
497
+ //新的运行任务-重新赋予新的任务id
498
+ if (index === 0) {
499
+ taskHash = generateUniqueHash().slice(0, 8);
500
+ }
501
+
502
+ if (isExist(errorExcelPath)) {
503
+ retryError++;
504
+
505
+ logger("开始运行错误列表");
506
+ jsonData = readExcel(errorExcelPath); // 数据来源于error excel
507
+ queue(index, jsonData, taskHash, fileHash);
508
+ return true;
509
+ }
510
+ return false;
511
+ };
512
+
513
+ const main = () => {
514
+ //当前任务hash
515
+ let hash = "";
516
+ let jsonData = [];
517
+ logger("》》》》》》》》》》视频批量任务任务开始《《《《《《《《《《" + hash);
518
+ logger("当前目录: " + workDir + ";工作目录: " + exeDir);
519
+
520
+ // 读取success.txt,判断是否是 异常中断,
521
+ // 并根据success日志获取最后一行的hash,找到对应的error_hash_excel
522
+
523
+ const successPath = path.join(workDir, "success.txt");
524
+ if (isExist(successPath)) {
525
+ const { loghash, lastItemIndex, fileName, parentHash } =
526
+ readTxt(successPath);
527
+
528
+ hash = loghash;
529
+ taskIndex = parseInt(lastItemIndex) + 1; // lastItemIndex为最后一项,所有要+1
530
+
531
+ //当前任务已经结束
532
+ if (fileName === "done") {
533
+ //找寻是否存在未上传的错误文件列表
534
+ const status = runErrorList(0, hash);
535
+ if (!status) {
536
+ logger(
537
+ "》》》》》》》》》》图片批量任务任务结束《《《《《《《《《《" + hash
538
+ );
539
+ }
540
+ } else {
541
+ //当前任务异常中断
542
+
543
+ //中断的任务类型为错误任务
544
+ if (parentHash) {
545
+ runErrorList(taskIndex, hash, parentHash);
546
+ } else {
547
+ jsonData = readExcel(excelDir);
548
+ queue(taskIndex, jsonData, hash);
549
+ }
550
+ }
551
+ } else {
552
+ hash = generateUniqueHash().slice(0, 8);
553
+ logger("成功日志不存在,开始新任务");
554
+
555
+ jsonData = readExcel(excelDir);
556
+ queue(taskIndex, jsonData, hash);
557
+ }
558
+ };
559
+
560
+ main();