@xiping/node-utils 1.0.21 → 1.0.36

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/lib/index.d.ts CHANGED
@@ -1,51 +1,4 @@
1
- /**
2
- * Represents a node in a directory tree structure
3
- * @interface Tree
4
- * @property {string} id - Unique identifier for the node (full path)
5
- * @property {string} label - Display name of the file or directory
6
- * @property {Tree[]} [children] - Optional array of child nodes for directories
7
- */
8
- export interface Tree {
9
- id: string;
10
- label: string;
11
- children?: Tree[];
12
- }
13
- /**
14
- * Recursively builds a tree structure from a directory
15
- * @param {string} dirPath - The absolute path to the directory to process
16
- * @param {string} [basePath] - The base path for generating relative paths (defaults to dirPath)
17
- * @returns {Tree} A tree structure representing the directory hierarchy
18
- * @throws Will throw an error if directory access fails
19
- *
20
- * @example
21
- * // Create a tree from a directory
22
- * const tree = buildDirectoryTree('/path/to/directory');
23
- *
24
- * // Structure of returned tree:
25
- * // {
26
- * // id: '/path/to/directory',
27
- * // label: 'directory',
28
- * // children: [
29
- * // { id: '/path/to/directory/file.txt', label: 'file.txt' },
30
- * // { id: '/path/to/directory/subdir', label: 'subdir', children: [...] }
31
- * // ]
32
- * // }
33
- */
34
- export declare function buildDirectoryTree(dirPath: string, basePath?: string): Tree;
35
- /**
36
- * Gets the parent directory path of a given directory path
37
- * Cross-platform compatible (Windows, macOS, Linux)
38
- * @param {string} dirPath - The path to get the parent directory from
39
- * @returns {string} The parent directory path
40
- *
41
- * @example
42
- * // On Unix-like systems (Mac/Linux)
43
- * getParentDirectory('/Users/documents/folder') // returns '/Users/documents'
44
- *
45
- * // On Windows
46
- * getParentDirectory('C:\\Users\\Documents\\folder') // returns 'C:\\Users\\Documents'
47
- *
48
- * // Works with both forward and backward slashes
49
- * getParentDirectory('C:/Users/Documents/folder') // returns 'C:/Users/Documents'
50
- */
51
- export declare function getParentDirectory(dirPath: string): string;
1
+ export * from "./src/directory-tree.js";
2
+ export * from "./src/path-utils.js";
3
+ export * from "./src/srt-to-vtt/index.js";
4
+ export * from "./src/ffmpeg/index.js";
package/lib/index.js CHANGED
@@ -1,85 +1,8 @@
1
- import { readdirSync, statSync } from "node:fs";
2
- import { join, dirname, normalize } from "node:path";
3
- /**
4
- * Recursively builds a tree structure from a directory
5
- * @param {string} dirPath - The absolute path to the directory to process
6
- * @param {string} [basePath] - The base path for generating relative paths (defaults to dirPath)
7
- * @returns {Tree} A tree structure representing the directory hierarchy
8
- * @throws Will throw an error if directory access fails
9
- *
10
- * @example
11
- * // Create a tree from a directory
12
- * const tree = buildDirectoryTree('/path/to/directory');
13
- *
14
- * // Structure of returned tree:
15
- * // {
16
- * // id: '/path/to/directory',
17
- * // label: 'directory',
18
- * // children: [
19
- * // { id: '/path/to/directory/file.txt', label: 'file.txt' },
20
- * // { id: '/path/to/directory/subdir', label: 'subdir', children: [...] }
21
- * // ]
22
- * // }
23
- */
24
- export function buildDirectoryTree(dirPath, basePath = dirPath) {
25
- const stats = statSync(dirPath);
26
- const normalizedDirPath = dirPath.replace(/\\/g, '/');
27
- const normalizedBasePath = basePath.replace(/\\/g, '/');
28
- const relativePath = normalizedDirPath.replace(normalizedBasePath, "").replace(/^\//, "") || "/";
29
- const fileName = normalizedDirPath.split("/").pop() || "/";
30
- const tree = {
31
- id: normalizedDirPath,
32
- label: fileName,
33
- };
34
- if (stats.isDirectory()) {
35
- try {
36
- const items = readdirSync(dirPath);
37
- if (items.length > 0) {
38
- tree.children = items
39
- .map((item) => {
40
- const itemPath = join(dirPath, item);
41
- try {
42
- return buildDirectoryTree(itemPath, basePath);
43
- }
44
- catch (error) {
45
- console.error(`Error processing ${itemPath}:`, error);
46
- return null;
47
- }
48
- })
49
- .filter((item) => item !== null);
50
- }
51
- }
52
- catch (error) {
53
- console.error(`Error reading directory ${dirPath}:`, error);
54
- }
55
- }
56
- console.log("tree=", tree);
57
- return tree;
58
- }
59
- // buildDirectoryTree(
60
- // "/Users/xipingwang/Documents/EF-code/e1-ai-assistant-it-web/src",
61
- // );
62
- /**
63
- * Gets the parent directory path of a given directory path
64
- * Cross-platform compatible (Windows, macOS, Linux)
65
- * @param {string} dirPath - The path to get the parent directory from
66
- * @returns {string} The parent directory path
67
- *
68
- * @example
69
- * // On Unix-like systems (Mac/Linux)
70
- * getParentDirectory('/Users/documents/folder') // returns '/Users/documents'
71
- *
72
- * // On Windows
73
- * getParentDirectory('C:\\Users\\Documents\\folder') // returns 'C:\\Users\\Documents'
74
- *
75
- * // Works with both forward and backward slashes
76
- * getParentDirectory('C:/Users/Documents/folder') // returns 'C:/Users/Documents'
77
- */
78
- export function getParentDirectory(dirPath) {
79
- // Normalize the path to handle different path formats
80
- const normalizedPath = normalize(dirPath);
81
- // Get the parent directory using dirname
82
- const parentPath = dirname(normalizedPath);
83
- // If we're already at the root, return the normalized root
84
- return normalizedPath === parentPath ? parentPath : parentPath;
85
- }
1
+ // 目录树相关功能
2
+ export * from "./src/directory-tree.js";
3
+ // 路径操作相关功能
4
+ export * from "./src/path-utils.js";
5
+ // SRT到VTT转换相关功能
6
+ export * from "./src/srt-to-vtt/index.js";
7
+ // FFmpeg相关功能
8
+ export * from "./src/ffmpeg/index.js";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Represents a node in a directory tree structure
3
+ * @interface Tree
4
+ * @property {string} id - Unique identifier for the node (full path)
5
+ * @property {string} label - Display name of the file or directory
6
+ * @property {Tree[]} [children] - Optional array of child nodes for directories
7
+ */
8
+ export interface Tree {
9
+ id: string;
10
+ label: string;
11
+ children?: Tree[];
12
+ }
13
+ /**
14
+ * Recursively builds a tree structure from a directory
15
+ * @param {string} dirPath - The absolute path to the directory to process
16
+ * @param {string} [basePath] - The base path for generating relative paths (defaults to dirPath)
17
+ * @returns {Tree} A tree structure representing the directory hierarchy
18
+ * @throws Will throw an error if directory access fails
19
+ *
20
+ * @example
21
+ * // Create a tree from a directory
22
+ * const tree = buildDirectoryTree('/path/to/directory');
23
+ *
24
+ * // Structure of returned tree:
25
+ * // {
26
+ * // id: '/path/to/directory',
27
+ * // label: 'directory',
28
+ * // children: [
29
+ * // { id: '/path/to/directory/file.txt', label: 'file.txt' },
30
+ * // { id: '/path/to/directory/subdir', label: 'subdir', children: [...] }
31
+ * // ]
32
+ * // }
33
+ */
34
+ export declare function buildDirectoryTree(dirPath: string, basePath?: string): Tree;
@@ -0,0 +1,58 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Recursively builds a tree structure from a directory
5
+ * @param {string} dirPath - The absolute path to the directory to process
6
+ * @param {string} [basePath] - The base path for generating relative paths (defaults to dirPath)
7
+ * @returns {Tree} A tree structure representing the directory hierarchy
8
+ * @throws Will throw an error if directory access fails
9
+ *
10
+ * @example
11
+ * // Create a tree from a directory
12
+ * const tree = buildDirectoryTree('/path/to/directory');
13
+ *
14
+ * // Structure of returned tree:
15
+ * // {
16
+ * // id: '/path/to/directory',
17
+ * // label: 'directory',
18
+ * // children: [
19
+ * // { id: '/path/to/directory/file.txt', label: 'file.txt' },
20
+ * // { id: '/path/to/directory/subdir', label: 'subdir', children: [...] }
21
+ * // ]
22
+ * // }
23
+ */
24
+ export function buildDirectoryTree(dirPath, basePath = dirPath) {
25
+ const stats = statSync(dirPath);
26
+ const normalizedDirPath = dirPath.replace(/\\/g, "/");
27
+ const normalizedBasePath = basePath.replace(/\\/g, "/");
28
+ const relativePath = normalizedDirPath.replace(normalizedBasePath, "").replace(/^\//, "") || "/";
29
+ const fileName = normalizedDirPath.split("/").pop() || "/";
30
+ const tree = {
31
+ id: normalizedDirPath,
32
+ label: fileName,
33
+ };
34
+ if (stats.isDirectory()) {
35
+ try {
36
+ const items = readdirSync(dirPath);
37
+ if (items.length > 0) {
38
+ tree.children = items
39
+ .map((item) => {
40
+ const itemPath = join(dirPath, item);
41
+ try {
42
+ return buildDirectoryTree(itemPath, basePath);
43
+ }
44
+ catch (error) {
45
+ console.error(`Error processing ${itemPath}:`, error);
46
+ return null;
47
+ }
48
+ })
49
+ .filter((item) => item !== null);
50
+ }
51
+ }
52
+ catch (error) {
53
+ console.error(`Error reading directory ${dirPath}:`, error);
54
+ }
55
+ }
56
+ console.log("tree=", tree);
57
+ return tree;
58
+ }
@@ -0,0 +1,6 @@
1
+ export declare function checkFFmpegAvailability(): boolean;
2
+ /**
3
+ * 检查 ffprobe 是否可用
4
+ * @returns 是否可用
5
+ */
6
+ export declare function isFfprobeAvailable(): boolean;
@@ -0,0 +1,14 @@
1
+ import * as shell from "shelljs";
2
+ // 检查 ffmpeg 可用性
3
+ export function checkFFmpegAvailability() {
4
+ const result = shell.exec('ffmpeg -version', { silent: true });
5
+ return result.code === 0;
6
+ }
7
+ /**
8
+ * 检查 ffprobe 是否可用
9
+ * @returns 是否可用
10
+ */
11
+ export function isFfprobeAvailable() {
12
+ const result = shell.exec("ffprobe -version", { silent: true });
13
+ return result.code === 0;
14
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 视频缩略图生成器
3
+ * 使用 ffmpeg 提取视频帧,使用 sharp 合成缩略图
4
+ */
5
+ interface ThumbnailOptions {
6
+ frames?: number;
7
+ outputWidth?: number;
8
+ columns?: number;
9
+ outputFileName?: string;
10
+ quality?: number;
11
+ format?: 'avif' | 'webp' | 'jpeg' | 'png';
12
+ batchSize?: number;
13
+ maxConcurrency?: number;
14
+ tempDir?: string;
15
+ }
16
+ interface ProcessingResult {
17
+ buffer: Buffer;
18
+ outputPath: string;
19
+ metadata: {
20
+ frames: number;
21
+ duration: number;
22
+ outputSize: {
23
+ width: number;
24
+ height: number;
25
+ };
26
+ };
27
+ }
28
+ export declare const getThumbnail: (videoPath: string, options?: ThumbnailOptions) => Promise<ProcessingResult>;
29
+ export {};
@@ -0,0 +1,226 @@
1
+ import * as shell from "shelljs";
2
+ import Sharp from "sharp";
3
+ import * as fs from "fs";
4
+ import * as path from "node:path";
5
+ import * as os from "node:os";
6
+ import { checkFFmpegAvailability } from "./check.js";
7
+ // 日志函数
8
+ const log = (message, data) => {
9
+ console.log(`[ThumbnailGenerator] ${message}`, data ? JSON.stringify(data, null, 2) : '');
10
+ };
11
+ // 验证和清理路径
12
+ function validateAndSanitizePath(inputPath) {
13
+ if (!inputPath || typeof inputPath !== 'string') {
14
+ throw new Error('Invalid path provided');
15
+ }
16
+ const resolvedPath = path.resolve(inputPath);
17
+ // 检查路径是否在允许的目录内
18
+ const cwd = process.cwd();
19
+ if (!resolvedPath.startsWith(cwd)) {
20
+ throw new Error('Path traversal attack detected');
21
+ }
22
+ return resolvedPath;
23
+ }
24
+ // 获取视频时长
25
+ function getDurationInSeconds(videoPath) {
26
+ log('Getting video duration', { videoPath });
27
+ const result = shell.exec(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`, { silent: true });
28
+ if (result.code !== 0) {
29
+ throw new Error(`FFprobe failed to get duration: ${result.stderr}`);
30
+ }
31
+ const duration = parseFloat(result.stdout);
32
+ if (isNaN(duration) || duration <= 0) {
33
+ throw new Error('Invalid video duration');
34
+ }
35
+ return duration;
36
+ }
37
+ // 并行提取视频帧
38
+ async function extractFrames(videoPath, tempDir, frames, interval, maxConcurrency = 4) {
39
+ log('Starting frame extraction', { frames, interval, maxConcurrency });
40
+ const framePromises = Array.from({ length: frames - 1 }, (_, i) => {
41
+ const t = interval * i;
42
+ const framePath = path.join(tempDir, `frame-${t}.jpg`);
43
+ return new Promise((resolve, reject) => {
44
+ const result = shell.exec(`ffmpeg -ss ${t} -i "${videoPath}" -vframes 1 -q:v 10 -an -threads 4 "${framePath}"`, { silent: true });
45
+ if (result.code === 0) {
46
+ log(`Frame ${i + 1}/${frames - 1} extracted`, { time: t });
47
+ resolve();
48
+ }
49
+ else {
50
+ reject(new Error(`FFmpeg failed for frame ${i + 1}: ${result.stderr}`));
51
+ }
52
+ });
53
+ });
54
+ // 分批执行以控制并发数
55
+ for (let i = 0; i < framePromises.length; i += maxConcurrency) {
56
+ const batch = framePromises.slice(i, i + maxConcurrency);
57
+ await Promise.all(batch);
58
+ }
59
+ }
60
+ // 创建缩略图
61
+ async function createThumbnail(frameFiles, tempDir, options) {
62
+ const { columns = 4, outputWidth = 3840, format = 'avif', quality = 80 } = options;
63
+ log('Creating thumbnail', { frameCount: frameFiles.length, columns, outputWidth });
64
+ // 计算尺寸
65
+ const rows = Math.ceil(frameFiles.length / columns);
66
+ const singleWidth = Math.floor(outputWidth / columns);
67
+ // 获取第一帧的元数据来计算宽高比
68
+ const firstFrame = await Sharp(path.join(tempDir, frameFiles[0])).metadata();
69
+ const aspectRatio = (firstFrame.width || 1) / (firstFrame.height || 1);
70
+ const singleHeight = Math.floor(singleWidth / aspectRatio);
71
+ log('Calculated dimensions', {
72
+ rows,
73
+ singleWidth,
74
+ singleHeight,
75
+ aspectRatio: aspectRatio.toFixed(2)
76
+ });
77
+ // 创建合成图像
78
+ const composite = Sharp({
79
+ create: {
80
+ width: singleWidth * columns,
81
+ height: singleHeight * rows,
82
+ channels: 3,
83
+ background: { r: 0, g: 0, b: 0 },
84
+ },
85
+ });
86
+ // 分批处理帧以避免内存问题
87
+ const batchSize = options.batchSize || 10;
88
+ const overlays = [];
89
+ for (let i = 0; i < frameFiles.length; i += batchSize) {
90
+ const batch = frameFiles.slice(i, i + batchSize);
91
+ const batchOverlays = await Promise.all(batch.map(async (file, batchIndex) => {
92
+ const index = i + batchIndex;
93
+ const row = Math.floor(index / columns);
94
+ const col = index % columns;
95
+ const resized = await Sharp(path.join(tempDir, file))
96
+ .resize(singleWidth, singleHeight, { fit: 'cover' })
97
+ .toBuffer();
98
+ return {
99
+ input: resized,
100
+ top: row * singleHeight,
101
+ left: col * singleWidth,
102
+ };
103
+ }));
104
+ overlays.push(...batchOverlays);
105
+ log(`Processed batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(frameFiles.length / batchSize)}`);
106
+ }
107
+ // 生成最终图像
108
+ let finalImage = composite.composite(overlays);
109
+ // 根据格式设置输出
110
+ switch (format) {
111
+ case 'avif':
112
+ finalImage = finalImage.avif({ quality });
113
+ break;
114
+ case 'webp':
115
+ finalImage = finalImage.webp({ quality });
116
+ break;
117
+ case 'jpeg':
118
+ finalImage = finalImage.jpeg({ quality });
119
+ break;
120
+ case 'png':
121
+ finalImage = finalImage.png({ quality });
122
+ break;
123
+ default:
124
+ finalImage = finalImage.avif({ quality });
125
+ }
126
+ return await finalImage.toBuffer();
127
+ }
128
+ // 临时目录管理
129
+ async function withTempDir(fn, customTempDir) {
130
+ const tempDir = customTempDir || fs.mkdtempSync(path.join(os.tmpdir(), "video-thumbnails-"));
131
+ try {
132
+ return await fn(tempDir);
133
+ }
134
+ finally {
135
+ if (fs.existsSync(tempDir) && !customTempDir) {
136
+ fs.rmSync(tempDir, { recursive: true, force: true });
137
+ log('Cleaned up temporary directory', { tempDir });
138
+ }
139
+ }
140
+ }
141
+ // 主函数
142
+ export const getThumbnail = async (videoPath, options = {}) => {
143
+ const startTime = Date.now();
144
+ try {
145
+ // 输入验证
146
+ const validatedPath = validateAndSanitizePath(videoPath);
147
+ if (!fs.existsSync(validatedPath)) {
148
+ throw new Error(`Video file not found: ${validatedPath}`);
149
+ }
150
+ // 检查 ffmpeg 可用性
151
+ if (!checkFFmpegAvailability()) {
152
+ throw new Error('FFmpeg is not available. Please install FFmpeg first.');
153
+ }
154
+ // 参数设置和验证
155
+ const { frames = 60, outputWidth = 3840, columns = 4, outputFileName = "thumbnail.avif", quality = 80, format = 'avif', batchSize = 10, maxConcurrency = 4, tempDir: customTempDir } = options;
156
+ // 参数验证
157
+ if (frames <= 0 || outputWidth <= 0 || columns <= 0) {
158
+ throw new Error('Invalid parameters: frames, outputWidth, and columns must be positive');
159
+ }
160
+ if (quality < 1 || quality > 100) {
161
+ throw new Error('Quality must be between 1 and 100');
162
+ }
163
+ log('Starting thumbnail generation', {
164
+ videoPath: validatedPath,
165
+ options: { frames, outputWidth, columns, format, quality }
166
+ });
167
+ // 获取视频信息
168
+ const durationInSeconds = getDurationInSeconds(validatedPath);
169
+ const interval = durationInSeconds / frames;
170
+ log('Video analysis complete', { durationInSeconds, interval });
171
+ // 准备输出路径
172
+ const videoDir = path.dirname(validatedPath);
173
+ const fileNameWithoutExt = path.basename(validatedPath, path.extname(validatedPath));
174
+ const outputPath = path.join(videoDir, `${fileNameWithoutExt}_${outputFileName}`);
175
+ // 使用临时目录处理
176
+ const result = await withTempDir(async (tempDir) => {
177
+ // 提取帧
178
+ await extractFrames(validatedPath, tempDir, frames, interval, maxConcurrency);
179
+ // 获取所有帧文件并排序
180
+ const frameFiles = fs
181
+ .readdirSync(tempDir)
182
+ .filter((file) => file.startsWith("frame-"))
183
+ .sort((a, b) => {
184
+ const numA = parseFloat(a.match(/\d+/)?.[0] || "0");
185
+ const numB = parseFloat(b.match(/\d+/)?.[0] || "0");
186
+ return numA - numB;
187
+ });
188
+ if (frameFiles.length === 0) {
189
+ throw new Error('No frames were extracted from the video');
190
+ }
191
+ log('Frame extraction complete', { extractedFrames: frameFiles.length });
192
+ // 创建缩略图
193
+ const buffer = await createThumbnail(frameFiles, tempDir, options);
194
+ return buffer;
195
+ }, customTempDir);
196
+ // 保存文件
197
+ fs.writeFileSync(outputPath, result);
198
+ const processingTime = Date.now() - startTime;
199
+ log('Thumbnail generation complete', {
200
+ outputPath,
201
+ processingTime: `${processingTime}ms`,
202
+ fileSize: `${(result.length / 1024).toFixed(2)}KB`
203
+ });
204
+ // 获取输出图像元数据
205
+ const metadata = await Sharp(result).metadata();
206
+ return {
207
+ buffer: result,
208
+ outputPath,
209
+ metadata: {
210
+ frames,
211
+ duration: durationInSeconds,
212
+ outputSize: {
213
+ width: metadata.width || 0,
214
+ height: metadata.height || 0
215
+ }
216
+ }
217
+ };
218
+ }
219
+ catch (error) {
220
+ log('Thumbnail generation failed', {
221
+ error: error instanceof Error ? error.message : String(error),
222
+ videoPath
223
+ });
224
+ throw error;
225
+ }
226
+ };
@@ -0,0 +1,49 @@
1
+ export * from "./getThumbnail.js";
2
+ export * from "./check.js";
3
+ /**
4
+ * 获取视频信息的接口定义
5
+ */
6
+ export interface VideoInfo {
7
+ /** 视频文件路径 */
8
+ path: string;
9
+ /** 视频时长(秒) */
10
+ duration: number;
11
+ /** 视频时长格式化字符串 */
12
+ durationFormatted: string;
13
+ /** 视频宽度 */
14
+ width: number;
15
+ /** 视频高度 */
16
+ height: number;
17
+ /** 视频编码格式 */
18
+ videoCodec: string;
19
+ /** 音频编码格式 */
20
+ audioCodec: string;
21
+ /** 视频比特率 */
22
+ bitrate: number;
23
+ /** 帧率 */
24
+ fps: number;
25
+ /** 文件大小(字节) */
26
+ fileSize: number;
27
+ /** 文件大小格式化字符串 */
28
+ fileSizeFormatted: string;
29
+ /** 原始 ffprobe 输出信息 */
30
+ rawInfo: string;
31
+ }
32
+ /**
33
+ * 获取视频信息(使用 ffprobe)
34
+ * @param videoPath 视频文件路径
35
+ * @returns 视频信息对象
36
+ */
37
+ export declare function getVideoInfo(videoPath: string): VideoInfo;
38
+ /**
39
+ * 批量获取视频信息
40
+ * @param videoPaths 视频文件路径数组
41
+ * @returns 视频信息对象数组
42
+ */
43
+ export declare function getMultipleVideoInfo(videoPaths: string[]): VideoInfo[];
44
+ /**
45
+ * 获取详细的视频信息(包含更多元数据)
46
+ * @param videoPath 视频文件路径
47
+ * @returns 详细的视频信息
48
+ */
49
+ export declare function getDetailedVideoInfo(videoPath: string): any;
@@ -0,0 +1,111 @@
1
+ import shell from "shelljs";
2
+ export * from "./getThumbnail.js";
3
+ export * from "./check.js";
4
+ /**
5
+ * 获取视频信息(使用 ffprobe)
6
+ * @param videoPath 视频文件路径
7
+ * @returns 视频信息对象
8
+ */
9
+ export function getVideoInfo(videoPath) {
10
+ // 检查文件是否存在
11
+ if (!shell.test("-f", videoPath)) {
12
+ throw new Error(`视频文件不存在: ${videoPath}`);
13
+ }
14
+ // 使用 ffprobe 获取 JSON 格式的视频信息
15
+ const result = shell.exec(`ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`, { silent: true });
16
+ if (result.code !== 0) {
17
+ throw new Error(`获取视频信息失败: ${result.stderr}`);
18
+ }
19
+ let probeData;
20
+ try {
21
+ probeData = JSON.parse(result.stdout);
22
+ }
23
+ catch (error) {
24
+ throw new Error(`解析 ffprobe 输出失败: ${error}`);
25
+ }
26
+ const info = {
27
+ path: videoPath,
28
+ rawInfo: JSON.stringify(probeData, null, 2),
29
+ };
30
+ // 从 format 信息中获取时长和文件大小
31
+ if (probeData.format) {
32
+ if (probeData.format.duration) {
33
+ info.duration = parseFloat(probeData.format.duration);
34
+ info.durationFormatted = formatDuration(info.duration);
35
+ }
36
+ if (probeData.format.size) {
37
+ info.fileSize = parseInt(probeData.format.size);
38
+ info.fileSizeFormatted = formatFileSize(info.fileSize);
39
+ }
40
+ }
41
+ // 从视频流中获取信息
42
+ const videoStream = probeData.streams?.find((stream) => stream.codec_type === "video");
43
+ if (videoStream) {
44
+ info.videoCodec = videoStream.codec_name || "unknown";
45
+ info.width = videoStream.width || 0;
46
+ info.height = videoStream.height || 0;
47
+ if (videoStream.r_frame_rate) {
48
+ const [num, den] = videoStream.r_frame_rate.split("/");
49
+ info.fps = parseInt(num) / parseInt(den);
50
+ }
51
+ if (videoStream.bit_rate) {
52
+ info.bitrate = parseInt(videoStream.bit_rate);
53
+ }
54
+ }
55
+ // 从音频流中获取信息
56
+ const audioStream = probeData.streams?.find((stream) => stream.codec_type === "audio");
57
+ if (audioStream) {
58
+ info.audioCodec = audioStream.codec_name || "unknown";
59
+ }
60
+ return info;
61
+ }
62
+ /**
63
+ * 格式化文件大小
64
+ * @param bytes 字节数
65
+ * @returns 格式化后的文件大小字符串
66
+ */
67
+ function formatFileSize(bytes) {
68
+ const units = ["B", "KB", "MB", "GB", "TB"];
69
+ let size = bytes;
70
+ let unitIndex = 0;
71
+ while (size >= 1024 && unitIndex < units.length - 1) {
72
+ size /= 1024;
73
+ unitIndex++;
74
+ }
75
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
76
+ }
77
+ /**
78
+ * 格式化时长
79
+ * @param seconds 秒数
80
+ * @returns 格式化后的时长字符串
81
+ */
82
+ function formatDuration(seconds) {
83
+ const hours = Math.floor(seconds / 3600);
84
+ const minutes = Math.floor((seconds % 3600) / 60);
85
+ const secs = Math.floor(seconds % 60);
86
+ const ms = Math.floor((seconds % 1) * 100);
87
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}.${ms.toString().padStart(2, "0")}`;
88
+ }
89
+ /**
90
+ * 批量获取视频信息
91
+ * @param videoPaths 视频文件路径数组
92
+ * @returns 视频信息对象数组
93
+ */
94
+ export function getMultipleVideoInfo(videoPaths) {
95
+ return videoPaths.map((path) => getVideoInfo(path));
96
+ }
97
+ /**
98
+ * 获取详细的视频信息(包含更多元数据)
99
+ * @param videoPath 视频文件路径
100
+ * @returns 详细的视频信息
101
+ */
102
+ export function getDetailedVideoInfo(videoPath) {
103
+ if (!shell.test("-f", videoPath)) {
104
+ throw new Error(`视频文件不存在: ${videoPath}`);
105
+ }
106
+ const result = shell.exec(`ffprobe -v quiet -print_format json -show_format -show_streams -show_chapters -show_private_data "${videoPath}"`, { silent: true });
107
+ if (result.code !== 0) {
108
+ throw new Error(`获取详细视频信息失败: ${result.stderr}`);
109
+ }
110
+ return JSON.parse(result.stdout);
111
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Gets the parent directory path of a given directory path
3
+ * Cross-platform compatible (Windows, macOS, Linux)
4
+ * @param {string} dirPath - The path to get the parent directory from
5
+ * @returns {string} The parent directory path
6
+ *
7
+ * @example
8
+ * // On Unix-like systems (Mac/Linux)
9
+ * getParentDirectory('/Users/documents/folder') // returns '/Users/documents'
10
+ *
11
+ * // On Windows
12
+ * getParentDirectory('C:\\Users\\Documents\\folder') // returns 'C:\\Users\\Documents'
13
+ *
14
+ * // Works with both forward and backward slashes
15
+ * getParentDirectory('C:/Users/Documents/folder') // returns 'C:/Users/Documents'
16
+ */
17
+ export declare function getParentDirectory(dirPath: string): string;
@@ -0,0 +1,25 @@
1
+ import { dirname, normalize } from "node:path";
2
+ /**
3
+ * Gets the parent directory path of a given directory path
4
+ * Cross-platform compatible (Windows, macOS, Linux)
5
+ * @param {string} dirPath - The path to get the parent directory from
6
+ * @returns {string} The parent directory path
7
+ *
8
+ * @example
9
+ * // On Unix-like systems (Mac/Linux)
10
+ * getParentDirectory('/Users/documents/folder') // returns '/Users/documents'
11
+ *
12
+ * // On Windows
13
+ * getParentDirectory('C:\\Users\\Documents\\folder') // returns 'C:\\Users\\Documents'
14
+ *
15
+ * // Works with both forward and backward slashes
16
+ * getParentDirectory('C:/Users/Documents/folder') // returns 'C:/Users/Documents'
17
+ */
18
+ export function getParentDirectory(dirPath) {
19
+ // Normalize the path to handle different path formats
20
+ const normalizedPath = normalize(dirPath);
21
+ // Get the parent directory using dirname
22
+ const parentPath = dirname(normalizedPath);
23
+ // If we're already at the root, return the normalized root
24
+ return normalizedPath === parentPath ? parentPath : parentPath;
25
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 从文件读取SRT内容并转换为VTT格式
3
+ * @param srtFilePath SRT文件路径
4
+ * @returns 转换后的VTT内容字符串
5
+ */
6
+ export declare function convertSrtFileToVtt(srtFilePath: string): string;
7
+ /**
8
+ * 从文件读取SRT内容并转换为VTT格式,同时保存到文件
9
+ * @param srtFilePath SRT文件路径
10
+ * @param outputPath 输出VTT文件路径(可选,默认与SRT文件同目录)
11
+ * @returns 输出文件路径
12
+ */
13
+ export declare function convertSrtFileToVttFile(srtFilePath: string, outputPath?: string): string;
14
+ /**
15
+ * 批量转换SRT文件到VTT格式
16
+ * @param srtFilePaths SRT文件路径数组
17
+ * @param outputDir 输出目录(可选)
18
+ * @returns 转换结果数组
19
+ */
20
+ export declare function batchConvertSrtToVtt(srtFilePaths: string[], outputDir?: string): Array<{
21
+ input: string;
22
+ output: string;
23
+ success: boolean;
24
+ error?: string;
25
+ }>;
26
+ /**
27
+ * 从字符串内容直接转换为VTT格式
28
+ * @param srtContent SRT内容字符串
29
+ * @returns VTT格式字符串
30
+ */
31
+ export declare function convertSrtStringToVtt(srtContent: string): string;
32
+ export { srtToVtt, validateSRT } from "@xiping/srt-to-vtt";
33
+ export default convertSrtFileToVtt;
@@ -0,0 +1,115 @@
1
+ import { srtToVtt, validateSRT } from "@xiping/srt-to-vtt";
2
+ import { readFileSync, writeFileSync, existsSync } from "fs";
3
+ import { join, extname, basename } from "path";
4
+ /**
5
+ * 从文件读取SRT内容并转换为VTT格式
6
+ * @param srtFilePath SRT文件路径
7
+ * @returns 转换后的VTT内容字符串
8
+ */
9
+ export function convertSrtFileToVtt(srtFilePath) {
10
+ try {
11
+ // 检查文件是否存在
12
+ if (!existsSync(srtFilePath)) {
13
+ throw new Error(`SRT文件不存在: ${srtFilePath}`);
14
+ }
15
+ // 检查文件扩展名
16
+ const ext = extname(srtFilePath).toLowerCase();
17
+ if (ext !== ".srt") {
18
+ throw new Error(`文件扩展名必须是.srt,当前为: ${ext}`);
19
+ }
20
+ // 读取SRT文件内容
21
+ const srtContent = readFileSync(srtFilePath, "utf-8");
22
+ // 验证SRT格式
23
+ if (!validateSRT(srtContent)) {
24
+ throw new Error("SRT文件格式无效");
25
+ }
26
+ // 转换为VTT格式
27
+ const vttContent = srtToVtt(srtContent);
28
+ return vttContent;
29
+ }
30
+ catch (error) {
31
+ throw new Error(`转换失败: ${error instanceof Error ? error.message : String(error)}`);
32
+ }
33
+ }
34
+ /**
35
+ * 从文件读取SRT内容并转换为VTT格式,同时保存到文件
36
+ * @param srtFilePath SRT文件路径
37
+ * @param outputPath 输出VTT文件路径(可选,默认与SRT文件同目录)
38
+ * @returns 输出文件路径
39
+ */
40
+ export function convertSrtFileToVttFile(srtFilePath, outputPath) {
41
+ try {
42
+ // 转换SRT到VTT
43
+ const vttContent = convertSrtFileToVtt(srtFilePath);
44
+ // 确定输出路径
45
+ let vttFilePath;
46
+ if (outputPath) {
47
+ vttFilePath = outputPath;
48
+ }
49
+ else {
50
+ // 默认输出到同目录,文件名相同但扩展名为.vtt
51
+ const dir = srtFilePath.substring(0, srtFilePath.lastIndexOf("/") + 1);
52
+ const baseName = basename(srtFilePath, ".srt");
53
+ vttFilePath = join(dir, `${baseName}.vtt`);
54
+ }
55
+ // 写入VTT文件
56
+ writeFileSync(vttFilePath, vttContent, "utf-8");
57
+ return vttFilePath;
58
+ }
59
+ catch (error) {
60
+ throw new Error(`文件转换失败: ${error instanceof Error ? error.message : String(error)}`);
61
+ }
62
+ }
63
+ /**
64
+ * 批量转换SRT文件到VTT格式
65
+ * @param srtFilePaths SRT文件路径数组
66
+ * @param outputDir 输出目录(可选)
67
+ * @returns 转换结果数组
68
+ */
69
+ export function batchConvertSrtToVtt(srtFilePaths, outputDir) {
70
+ const results = [];
71
+ for (const srtFilePath of srtFilePaths) {
72
+ try {
73
+ let outputPath;
74
+ if (outputDir) {
75
+ const baseName = basename(srtFilePath, ".srt");
76
+ outputPath = join(outputDir, `${baseName}.vtt`);
77
+ }
78
+ const vttFilePath = convertSrtFileToVttFile(srtFilePath, outputPath);
79
+ results.push({
80
+ input: srtFilePath,
81
+ output: vttFilePath,
82
+ success: true,
83
+ });
84
+ }
85
+ catch (error) {
86
+ results.push({
87
+ input: srtFilePath,
88
+ output: "",
89
+ success: false,
90
+ error: error instanceof Error ? error.message : String(error),
91
+ });
92
+ }
93
+ }
94
+ return results;
95
+ }
96
+ /**
97
+ * 从字符串内容直接转换为VTT格式
98
+ * @param srtContent SRT内容字符串
99
+ * @returns VTT格式字符串
100
+ */
101
+ export function convertSrtStringToVtt(srtContent) {
102
+ try {
103
+ if (!validateSRT(srtContent)) {
104
+ throw new Error("SRT内容格式无效");
105
+ }
106
+ return srtToVtt(srtContent);
107
+ }
108
+ catch (error) {
109
+ throw new Error(`转换失败: ${error instanceof Error ? error.message : String(error)}`);
110
+ }
111
+ }
112
+ // 重新导出原始包的功能
113
+ export { srtToVtt, validateSRT } from "@xiping/srt-to-vtt";
114
+ // 默认导出主转换函数
115
+ export default convertSrtFileToVtt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiping/node-utils",
3
- "version": "1.0.21",
3
+ "version": "1.0.36",
4
4
  "description": "node-utils",
5
5
  "type": "module",
6
6
  "author": "The-End-Hero <527409987@qq.com>",
@@ -25,11 +25,17 @@
25
25
  "bugs": {
26
26
  "url": "https://github.com/The-End-Hero/wang-ping/issues"
27
27
  },
28
- "gitHead": "7c9ee2b2805466754ebc085097797db105ee2283",
28
+ "gitHead": "79ba6360ab08693c43b9d0cc3dace1e7baba0421",
29
29
  "publishConfig": {
30
30
  "access": "public",
31
31
  "registry": "https://registry.npmjs.org/"
32
32
  },
33
+ "dependencies": {
34
+ "@xiping/srt-to-vtt": "1.0.35",
35
+ "chalk": "^5.5.0",
36
+ "sharp": "^0.34.3",
37
+ "shelljs": "0.10.0"
38
+ },
33
39
  "devDependencies": {
34
40
  "@types/node": "^22.15.34",
35
41
  "tslib": "^2.8.1",