Bun Spawn -> node child process in tools and seed versioning

This commit is contained in:
2026-02-25 02:15:54 +00:00
parent 21fde3d989
commit f2da4c0300
8 changed files with 107 additions and 74 deletions
+9 -6
View File
@@ -1,7 +1,7 @@
---
name: Convert To MP3
description: Convert audio files to MP3 320kbps, preserving metadata.
version: 2
version: 3
author: pastilhas
tags:
- audio
@@ -38,10 +38,13 @@ inputs:
Convert audio files to MP3 320kbps, preserving metadata.
## Important
- Do NOT explore, list, or inspect the target path before converting. The tool handles everything internally — file discovery, format detection, and error reporting.
- Do NOT use bash, ls, or any other tool. Only use `convert_audio_to_mp3`.
- Call the tool exactly once, then report the result. Nothing else.
## Steps
1. Determine whether `file_path` points to a single audio file or a directory.
2. Call `convert_audio_to_mp3` with the appropriate mode:
- Single file: `mode="single"`, `path=file_path`
- Directory: `mode="batch"`, `path=file_path`
3. Report the result to the user.
1. If `file_path` has an audio extension (flac, wav, ogg, etc.), call `convert_audio_to_mp3(mode="single", path=file_path)`. Otherwise call `convert_audio_to_mp3(mode="batch", path=file_path)`.
2. Print the tool's output as the final report. Do not add extra commentary.
+1
View File
@@ -2,6 +2,7 @@
name: convert_audio_to_mp3
label: Convert Audio to MP3
description: Convert audio files to MP3 320kbps using ffmpeg, preserving metadata. Supports single file conversion with real-time percentage progress, and batch conversion of an entire artist directory with per-file progress. Use when the user wants to convert FLAC, WAV, OGG, or other audio formats to MP3.
version: 2
language: typescript
inputs:
mode:
+47 -48
View File
@@ -1,5 +1,6 @@
import { readdirSync, existsSync, unlinkSync, statSync } from 'node:fs';
import { join, extname, basename } from 'node:path';
import { spawn, execFile } from 'node:child_process';
const AUDIO_EXTS = new Set([
'flac', 'wav', 'ogg', 'wma', 'aac', 'm4a', 'opus',
@@ -13,14 +14,17 @@ function update(onUpdate: OnUpdate | undefined, text: string): void {
}
async function getDuration(filePath: string): Promise<number | null> {
const proc = Bun.spawn(
['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', filePath],
{ stdout: 'pipe', stderr: 'pipe' },
);
const text = await new Response(proc.stdout).text();
await proc.exited;
const n = parseFloat(text.trim());
return isNaN(n) ? null : n;
return new Promise((resolve) => {
execFile(
'ffprobe',
['-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', filePath],
(err, stdout) => {
if (err) return resolve(null);
const n = parseFloat(stdout.trim());
resolve(isNaN(n) ? null : n);
},
);
});
}
function scanAudioFiles(dir: string): string[] {
@@ -57,60 +61,55 @@ async function convertFile(
return { outputFile, success: true, skipped: true };
}
const proc = Bun.spawn(
[
'ffmpeg',
return new Promise((resolve) => {
const proc = spawn('ffmpeg', [
'-i', inputFile,
'-progress', 'pipe:1', // progress data → stdout
'-progress', 'pipe:1',
'-nostats',
'-loglevel', 'error', // only errors → stderr
'-loglevel', 'error',
'-codec:a', 'libmp3lame',
'-b:a', '320k',
outputFile,
'-y',
],
{ stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' },
);
], { stdio: ['ignore', 'pipe', 'pipe'] });
// Parse -progress output from stdout for real-time percentage
const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
const decoder = new TextDecoder();
let buf = '';
let lastPercent = -1;
let buf = '';
let lastPercent = -1;
let stderrText = '';
(async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop() ?? '';
for (const line of lines) {
const m = line.match(/^out_time_us=(\d+)$/);
if (m && duration) {
const pct = Math.min(100, Math.round((parseInt(m[1]!, 10) / 1_000_000 / duration) * 100));
if (pct >= lastPercent + 5) {
lastPercent = pct;
update(onUpdate, `${label} ${pct}%`);
}
proc.stdout.on('data', (chunk: Buffer) => {
buf += chunk.toString();
const lines = buf.split('\n');
buf = lines.pop() ?? '';
for (const line of lines) {
const m = line.match(/^out_time_us=(\d+)$/);
if (m && duration) {
const pct = Math.min(100, Math.round((parseInt(m[1]!, 10) / 1_000_000 / duration) * 100));
if (pct >= lastPercent + 5) {
lastPercent = pct;
update(onUpdate, `${label} ${pct}%`);
}
}
}
} catch {
// stream closed
}
})();
});
const stderrText = await new Response(proc.stderr).text();
await proc.exited;
proc.stderr.on('data', (chunk: Buffer) => {
stderrText += chunk.toString();
});
if (proc.exitCode !== 0) {
if (existsSync(outputFile)) unlinkSync(outputFile);
return { outputFile, success: false, error: stderrText.trim() || 'ffmpeg failed' };
}
proc.on('close', (code) => {
if (code !== 0) {
if (existsSync(outputFile)) unlinkSync(outputFile);
resolve({ outputFile, success: false, error: stderrText.trim() || 'ffmpeg failed' });
} else {
resolve({ outputFile, success: true });
}
});
return { outputFile, success: true };
proc.on('error', (err) => {
resolve({ outputFile, success: false, error: err.message });
});
});
}
export async function execute(