@xiping/node-utils 1.0.61 → 1.0.62

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/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @xiping/node-utils
2
+
3
+ Node.js 通用工具库,提供目录树、路径、SRT→VTT 字幕转换、FFmpeg 视频处理、文件信息与图片处理等能力。
4
+
5
+ ```bash
6
+ npm install @xiping/node-utils
7
+ ```
8
+
9
+ ## 使用案例
10
+
11
+ ```typescript
12
+ import {
13
+ buildDirectoryTree,
14
+ getParentDirectory,
15
+ convertSrtFileToVttFile,
16
+ getVideoInfo,
17
+ getThumbnail,
18
+ getFileConfig,
19
+ convertImage,
20
+ checkFFmpegAvailability,
21
+ } from '@xiping/node-utils';
22
+
23
+ // 目录树
24
+ const tree = buildDirectoryTree('/path/to/dir');
25
+
26
+ // 路径
27
+ const parent = getParentDirectory('/Users/documents/folder');
28
+
29
+ // SRT 转 VTT
30
+ convertSrtFileToVttFile('/path/to/subtitle.srt');
31
+
32
+ // 视频信息与缩略图(需系统安装 ffmpeg)
33
+ if (checkFFmpegAvailability()) {
34
+ const info = getVideoInfo('/path/to/video.mp4');
35
+ const thumb = await getThumbnail('/path/to/video.mp4', { frames: 30 });
36
+ }
37
+
38
+ // 文件配置(含视频元数据)
39
+ const config = getFileConfig('/path/to/file.mp4');
40
+
41
+ // 图片格式转换
42
+ await convertImage(inputPath, outputPath, 'webp', { quality: 80 });
43
+ ```
44
+
45
+ 更多说明见 [src/ffmpeg/README.md](./src/ffmpeg/README.md)、[src/image/README.md](./src/image/README.md)、[src/srt-to-vtt/README.md](./src/srt-to-vtt/README.md)。
@@ -18,8 +18,7 @@
18
18
 
19
19
  ### 依赖包
20
20
 
21
- - Node.js
22
- - [shelljs](https://www.npmjs.com/package/shelljs)
21
+ - Node.js(使用内置 `child_process.spawnSync` 调用 ffmpeg/ffprobe,兼容 Electron、跨平台)
23
22
  - [sharp](https://www.npmjs.com/package/sharp)(仅缩略图功能需要)
24
23
 
25
24
  ### 系统要求
@@ -1,7 +1,7 @@
1
- import shell from "shelljs";
1
+ import { runSync } from "./runSync.js";
2
2
  // 检查 ffmpeg 可用性
3
3
  export function checkFFmpegAvailability() {
4
- const result = shell.exec('ffmpeg -version', { silent: true });
4
+ const result = runSync("ffmpeg", ["-version"]);
5
5
  return result.code === 0;
6
6
  }
7
7
  /**
@@ -9,6 +9,6 @@ export function checkFFmpegAvailability() {
9
9
  * @returns 是否可用
10
10
  */
11
11
  export function isFfprobeAvailable() {
12
- const result = shell.exec("ffprobe -version", { silent: true });
12
+ const result = runSync("ffprobe", ["-version"]);
13
13
  return result.code === 0;
14
14
  }
@@ -1,5 +1,5 @@
1
- import shell from "shelljs";
2
1
  import * as fs from "fs";
2
+ import { runSync } from "./runSync.js";
3
3
  import * as path from "node:path";
4
4
  import * as os from "node:os";
5
5
  import { checkFFmpegAvailability } from "./check.js";
@@ -40,7 +40,12 @@ function normalizeTimeFormat(time) {
40
40
  // 获取视频时长
41
41
  function getDurationInSeconds(videoPath) {
42
42
  log('Getting video duration', { videoPath });
43
- const result = shell.exec(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`, { silent: true });
43
+ const result = runSync("ffprobe", [
44
+ "-v", "error",
45
+ "-show_entries", "format=duration",
46
+ "-of", "default=noprint_wrappers=1:nokey=1",
47
+ videoPath,
48
+ ]);
44
49
  if (result.code !== 0) {
45
50
  throw new Error(`FFprobe failed to get duration: ${result.stderr}`);
46
51
  }
@@ -104,20 +109,6 @@ function timeToSeconds(timeStr) {
104
109
  }
105
110
  throw new Error(`Invalid time format: ${timeStr}`);
106
111
  }
107
- // 构建 FFmpeg 命令
108
- function buildFFmpegCommand(inputPath, outputPath, startTime, duration, options) {
109
- const { overwrite = false } = options;
110
- // 使用 copy 模式,保持原视频编码和质量
111
- let command = `ffmpeg -ss ${startTime} -i "${inputPath}" -t ${duration}`;
112
- // 复制视频和音频流,不重新编码
113
- command += ' -c copy';
114
- // 其他设置
115
- command += ' -avoid_negative_ts make_zero';
116
- command += ' -y'; // 自动覆盖输出文件
117
- // 输出文件
118
- command += ` "${outputPath}"`;
119
- return command;
120
- }
121
112
  // 临时目录管理
122
113
  async function withTempDir(fn, customTempDir) {
123
114
  const tempDir = customTempDir || fs.mkdtempSync(path.join(os.tmpdir(), "video-cut-"));
@@ -169,11 +160,16 @@ export const cutVideo = async (videoPath, options = {}) => {
169
160
  // 使用临时目录处理
170
161
  await withTempDir(async (tempDir) => {
171
162
  const tempOutputPath = path.join(tempDir, `temp_output.${outputFormat}`);
172
- // 构建 FFmpeg 命令
173
- const command = buildFFmpegCommand(validatedPath, tempOutputPath, timeParams.startTime, timeParams.duration, options);
174
- log('Executing FFmpeg command', { command });
175
- // 执行 FFmpeg 命令
176
- const result = shell.exec(command, { silent: true });
163
+ log('Executing FFmpeg command', { startTime: timeParams.startTime, duration: timeParams.duration });
164
+ const result = runSync("ffmpeg", [
165
+ "-ss", timeParams.startTime,
166
+ "-i", validatedPath,
167
+ "-t", timeParams.duration,
168
+ "-c", "copy",
169
+ "-avoid_negative_ts", "make_zero",
170
+ "-y",
171
+ tempOutputPath,
172
+ ]);
177
173
  if (result.code !== 0) {
178
174
  throw new Error(`FFmpeg failed: ${result.stderr}`);
179
175
  }
@@ -1,5 +1,5 @@
1
- import shell from "shelljs";
2
1
  import { spawn } from "node:child_process";
2
+ import { runSync } from "./runSync.js";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import * as os from "node:os";
@@ -18,7 +18,12 @@ function validateAndSanitizePath(inputPath) {
18
18
  return inputPath;
19
19
  }
20
20
  function getDurationInSeconds(videoPath) {
21
- const result = shell.exec(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`, { silent: true });
21
+ const result = runSync("ffprobe", [
22
+ "-v", "error",
23
+ "-show_entries", "format=duration",
24
+ "-of", "default=noprint_wrappers=1:nokey=1",
25
+ videoPath,
26
+ ]);
22
27
  if (result.code !== 0) {
23
28
  throw new Error(`FFprobe failed to get duration: ${result.stderr}`);
24
29
  }
@@ -57,13 +62,12 @@ function buildFfmpegArgs(outputPath, outputFormat, copyStream, audioCodec) {
57
62
  args.push("-y", outputPath);
58
63
  return args;
59
64
  }
60
- /** 执行 ffmpeg(shell.exec),无进度 */
65
+ /** 执行 ffmpeg(runSync),无进度 */
61
66
  function runFfmpegSync(inputPath, outputPath, outputFormat, copyStream, audioCodec) {
62
67
  const args = buildFfmpegArgs(outputPath, outputFormat, copyStream, audioCodec);
63
68
  args[1] = inputPath;
64
- const command = `ffmpeg ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`;
65
- log("Executing FFmpeg command", { command });
66
- const result = shell.exec(command, { silent: true });
69
+ log("Executing FFmpeg command", { inputPath, outputPath });
70
+ const result = runSync("ffmpeg", args);
67
71
  if (result.code !== 0) {
68
72
  throw new Error(`FFmpeg failed: ${result.stderr}`);
69
73
  }
@@ -1,5 +1,5 @@
1
- import shell from "shelljs";
2
1
  import Sharp from "sharp";
2
+ import { runSync } from "./runSync.js";
3
3
  import * as fs from "fs";
4
4
  import * as path from "node:path";
5
5
  import * as os from "node:os";
@@ -22,7 +22,12 @@ function validateAndSanitizePath(inputPath) {
22
22
  // 获取视频时长
23
23
  function getDurationInSeconds(videoPath) {
24
24
  log('Getting video duration', { videoPath });
25
- const result = shell.exec(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`, { silent: true });
25
+ const result = runSync("ffprobe", [
26
+ "-v", "error",
27
+ "-show_entries", "format=duration",
28
+ "-of", "default=noprint_wrappers=1:nokey=1",
29
+ videoPath,
30
+ ]);
26
31
  if (result.code !== 0) {
27
32
  throw new Error(`FFprobe failed to get duration: ${result.stderr}`);
28
33
  }
@@ -40,7 +45,15 @@ async function extractFrames(videoPath, tempDir, frames, interval, maxConcurrenc
40
45
  const t = interval * i;
41
46
  const framePath = path.join(tempDir, `frame-${t}.jpg`);
42
47
  return new Promise((resolve, reject) => {
43
- const result = shell.exec(`ffmpeg -ss ${t} -i "${videoPath}" -vframes 1 -q:v 10 -an -threads 4 "${framePath}"`, { silent: true });
48
+ const result = runSync("ffmpeg", [
49
+ "-ss", String(t),
50
+ "-i", videoPath,
51
+ "-vframes", "1",
52
+ "-q:v", "10",
53
+ "-an",
54
+ "-threads", "4",
55
+ framePath,
56
+ ]);
44
57
  if (result.code === 0) {
45
58
  log(`Frame ${i + 1}/${total} extracted`, { time: t });
46
59
  resolve();
@@ -1,4 +1,5 @@
1
- import shell from "shelljs";
1
+ import * as fs from "node:fs";
2
+ import { runSync } from "./runSync.js";
2
3
  /**
3
4
  * 格式化文件大小
4
5
  * @param bytes 字节数
@@ -32,12 +33,16 @@ function formatDuration(seconds) {
32
33
  * @returns 视频信息对象
33
34
  */
34
35
  export function getVideoInfo(videoPath) {
35
- // 检查文件是否存在
36
- if (!shell.test("-f", videoPath)) {
36
+ if (!fs.existsSync(videoPath)) {
37
37
  throw new Error(`视频文件不存在: ${videoPath}`);
38
38
  }
39
- // 使用 ffprobe 获取 JSON 格式的视频信息
40
- const result = shell.exec(`ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`, { silent: true });
39
+ const result = runSync("ffprobe", [
40
+ "-v", "quiet",
41
+ "-print_format", "json",
42
+ "-show_format",
43
+ "-show_streams",
44
+ videoPath,
45
+ ]);
41
46
  if (result.code !== 0) {
42
47
  throw new Error(`获取视频信息失败: ${result.stderr}`);
43
48
  }
@@ -98,10 +103,18 @@ export function getMultipleVideoInfo(videoPaths) {
98
103
  * @returns 详细的视频信息
99
104
  */
100
105
  export function getDetailedVideoInfo(videoPath) {
101
- if (!shell.test("-f", videoPath)) {
106
+ if (!fs.existsSync(videoPath)) {
102
107
  throw new Error(`视频文件不存在: ${videoPath}`);
103
108
  }
104
- const result = shell.exec(`ffprobe -v quiet -print_format json -show_format -show_streams -show_chapters -show_private_data "${videoPath}"`, { silent: true });
109
+ const result = runSync("ffprobe", [
110
+ "-v", "quiet",
111
+ "-print_format", "json",
112
+ "-show_format",
113
+ "-show_streams",
114
+ "-show_chapters",
115
+ "-show_private_data",
116
+ videoPath,
117
+ ]);
105
118
  if (result.code !== 0) {
106
119
  throw new Error(`获取详细视频信息失败: ${result.stderr}`);
107
120
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 同步执行外部命令(跨平台、兼容 Electron,不依赖 shell)。
3
+ * 使用 spawnSync + 参数数组,避免 shell 解析和路径/空格问题。
4
+ *
5
+ * @param command 可执行文件名(如 ffmpeg、ffprobe)
6
+ * @param args 参数列表(路径无需额外引号)
7
+ * @returns 与 shelljs.exec 兼容的 { code, stdout, stderr }
8
+ */
9
+ export declare function runSync(command: string, args: string[], options?: {
10
+ encoding?: BufferEncoding;
11
+ }): {
12
+ code: number;
13
+ stdout: string;
14
+ stderr: string;
15
+ };
@@ -0,0 +1,20 @@
1
+ import { spawnSync } from "node:child_process";
2
+ /**
3
+ * 同步执行外部命令(跨平台、兼容 Electron,不依赖 shell)。
4
+ * 使用 spawnSync + 参数数组,避免 shell 解析和路径/空格问题。
5
+ *
6
+ * @param command 可执行文件名(如 ffmpeg、ffprobe)
7
+ * @param args 参数列表(路径无需额外引号)
8
+ * @returns 与 shelljs.exec 兼容的 { code, stdout, stderr }
9
+ */
10
+ export function runSync(command, args, options) {
11
+ const encoding = options?.encoding ?? "utf8";
12
+ const result = spawnSync(command, args, {
13
+ encoding,
14
+ windowsHide: true,
15
+ });
16
+ const stdout = (result.stdout ?? "");
17
+ const stderr = (result.stderr ?? "");
18
+ const code = result.status ?? (result.signal ? -1 : 0);
19
+ return { code, stdout, stderr };
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiping/node-utils",
3
- "version": "1.0.61",
3
+ "version": "1.0.62",
4
4
  "description": "node-utils",
5
5
  "type": "module",
6
6
  "author": "The-End-Hero <527409987@qq.com>",
@@ -16,16 +16,16 @@
16
16
  ],
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "git+https://github.com/The-End-Hero/wang-ping.git"
19
+ "url": "git+https://github.com/The-End-Hero/xiping.git"
20
20
  },
21
21
  "scripts": {
22
22
  "test": "echo \"Error: run tests from root\" && exit 1",
23
23
  "build": "tsc && node scripts/copy-readmes.js"
24
24
  },
25
25
  "bugs": {
26
- "url": "https://github.com/The-End-Hero/wang-ping/issues"
26
+ "url": "https://github.com/The-End-Hero/xiping/issues"
27
27
  },
28
- "gitHead": "6cb2ac8c64a780abb18ec793978d3adfff71fba7",
28
+ "gitHead": "83ac9cffc81c4bbd5b78e1ecf621029630c98c20",
29
29
  "publishConfig": {
30
30
  "access": "public",
31
31
  "registry": "https://registry.npmjs.org/"
@@ -33,14 +33,12 @@
33
33
  "dependencies": {
34
34
  "@xiping/subtitle": "1.0.52",
35
35
  "chalk": "^5.6.2",
36
- "fs-extra": "^11.3.3",
37
- "sharp": "^0.34.4",
38
- "shelljs": "0.10.0",
36
+ "sharp": "^0.34.5",
39
37
  "srt-parser-2": "^1.2.3"
40
38
  },
41
39
  "devDependencies": {
42
- "@types/node": "^24.10.4",
40
+ "@types/node": "^24.10.13",
43
41
  "tslib": "^2.8.1",
44
- "typescript": "^5.9.2"
42
+ "typescript": "^5.9.3"
45
43
  }
46
44
  }