Merge branch 'worktree-library-modernization'

This commit is contained in:
MythEclipse
2026-05-15 04:34:41 +07:00
12 changed files with 787 additions and 450 deletions
+61
View File
@@ -0,0 +1,61 @@
import { spawn } from "child_process";
export interface MuxFfmpegArgsOptions {
inputs: string[];
filter: string;
output: string;
codec: string;
audioFrequency?: number;
audioChannels?: number;
}
/**
* Builds ffmpeg argument array for muxing audio clips.
*/
export function buildMuxFfmpegArgs(options: MuxFfmpegArgsOptions): string[] {
const args: string[] = ["-y"];
for (const input of options.inputs) {
args.push("-i", input);
}
args.push("-filter_complex", options.filter);
args.push("-map", "[out]");
args.push("-codec:a", options.codec);
if (options.audioFrequency !== undefined) {
args.push("-ar", String(options.audioFrequency));
}
if (options.audioChannels !== undefined) {
args.push("-ac", String(options.audioChannels));
}
args.push(options.output);
return args;
}
/**
* Runs ffmpeg with the given arguments.
* Resolves on successful (code 0) exit, rejects on error or non-zero exit.
*/
export function runFfmpeg(args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn("ffmpeg", args, {
stdio: ["ignore", "inherit", "inherit"],
});
proc.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`ffmpeg exited with code ${code}`));
}
});
proc.on("error", (err) => {
reject(err);
});
});
}
+16 -13
View File
@@ -5,13 +5,11 @@ import { migrate as migrateSqlite } from "drizzle-orm/better-sqlite3/migrator";
import { migrate as migratePostgres } from "drizzle-orm/node-postgres/migrator";
import { config } from "../config";
import { createChildLogger } from "../logger";
import { initializeDatabase } from "./drizzle";
import { closeDatabase, initializeDatabase } from "./drizzle";
const logger = createChildLogger("migrate");
export async function initializeMigrationSqliteDatabase(
path = ".muxer-queue.db",
) {
export function initializeMigrationSqliteDatabase(path = ".muxer-queue.db") {
const sqlite = new Database(path);
sqlite.pragma("journal_mode = WAL");
return { sqlite, db: drizzleSqlite(sqlite) };
@@ -23,17 +21,23 @@ export async function runMigrations(): Promise<void> {
if (config.DATABASE_TYPE === "postgres") {
logger.info("Running PostgreSQL migrations");
const db = await initializeDatabase();
await migratePostgres(db as any, {
migrationsFolder: "./drizzle/migrations",
});
const db = (await initializeDatabase()) as Parameters<
typeof migratePostgres
>[0];
try {
await migratePostgres(db, { migrationsFolder: "./drizzle/migrations" });
} finally {
await closeDatabase();
}
logger.info("PostgreSQL migrations completed successfully");
} else {
logger.info("Running SQLite migrations");
const { sqlite, db } = await initializeMigrationSqliteDatabase();
migrateSqlite(db, { migrationsFolder: "./drizzle/migrations" });
// Ensure the SQLite connection is closed after migrations
sqlite.close();
const { sqlite, db } = initializeMigrationSqliteDatabase();
try {
migrateSqlite(db, { migrationsFolder: "./drizzle/migrations" });
} finally {
sqlite.close();
}
logger.info("SQLite migrations completed successfully");
}
} catch (error) {
@@ -45,7 +49,6 @@ export async function runMigrations(): Promise<void> {
}
}
// Run migrations if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
runMigrations()
.then(() => {
+21 -28
View File
@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs";
import path from "path";
import { buildMuxFfmpegArgs, runFfmpeg } from "./audio/ffmpegProcess";
const recordingsDir = process.env.RECORDINGS_DIR ?? "./recordings";
@@ -73,7 +73,7 @@ async function startMuxingToAup3() {
// Check if OGG file has valid header (starts with "OggS")
const oggBuffer = fs.readFileSync(oggPath);
const oggHeader = oggBuffer.slice(0, 4).toString();
const oggHeader = oggBuffer.subarray(0, 4).toString();
if (oggHeader !== "OggS") {
console.warn(
`[muxer-aup3] Skipping invalid OGG file (bad header): ${oggPath}`,
@@ -116,15 +116,12 @@ async function startMuxingToAup3() {
`[muxer-aup3] Found ${clips.length} clips. Base timestamp: ${globalStartTime}`,
);
const command = ffmpeg();
const filterParts: string[] = [];
console.log(
`[muxer-aup3] Creating audio filters for ${clips.length} clips...`,
);
clips.forEach((clip, index) => {
command.input(clip.oggPath);
// Calculate delay relative to the global start time
const delayMs = clip.meta.startTime - globalStartTime;
@@ -154,33 +151,29 @@ async function startMuxingToAup3() {
const timestamp = Date.now();
const wavFilename = path.join(recordingsDir, `muxed-${timestamp}.wav`);
const aup3Filename = path.join(recordingsDir, `muxed-${timestamp}.aup3`);
const inputs = clips.map((clip) => clip.oggPath);
console.log(
`[muxer-aup3] Combining clips to WAV. This might take a while...`,
);
// Using fluent-ffmpeg's complexFilter
command
.complexFilter(filterParts, "out")
.audioCodec("pcm_s16le")
.audioFrequency(44100)
.audioChannels(2)
.save(wavFilename)
.on("progress", (progress) => {
if (progress.percent) {
console.log(
`[muxer-aup3] WAV Progress: ${progress.percent.toFixed(2)}%`,
);
}
})
.on("end", () => {
console.log(`[muxer-aup3] WAV file created: ${wavFilename}`);
console.log(`[muxer-aup3] Creating AUP3 project file...`);
createAup3Project(wavFilename, aup3Filename, clips, globalStartTime);
})
.on("error", (err) => {
console.error(`[muxer-aup3] FFmpeg Error:`, err);
try {
const args = buildMuxFfmpegArgs({
inputs,
filter: filterParts.join(";"),
output: wavFilename,
codec: "pcm_s16le",
audioFrequency: 44100,
audioChannels: 2,
});
await runFfmpeg(args);
console.log(`[muxer-aup3] WAV file created: ${wavFilename}`);
console.log(`[muxer-aup3] Creating AUP3 project file...`);
createAup3Project(wavFilename, aup3Filename, clips, globalStartTime);
} catch (err) {
console.error(`[muxer-aup3] FFmpeg Error:`, err);
}
}
function createAup3Project(
+19 -24
View File
@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs";
import path from "path";
import { buildMuxFfmpegArgs, runFfmpeg } from "./audio/ffmpegProcess";
const recordingsDir = process.env.RECORDINGS_DIR ?? "./recordings";
@@ -71,7 +71,7 @@ async function startMuxing() {
// Check if OGG file has valid header (starts with "OggS")
const oggBuffer = fs.readFileSync(oggPath);
const oggHeader = oggBuffer.slice(0, 4).toString();
const oggHeader = oggBuffer.subarray(0, 4).toString();
if (oggHeader !== "OggS") {
console.warn(
`[muxer] Skipping invalid OGG file (bad header): ${oggPath}`,
@@ -114,13 +114,10 @@ async function startMuxing() {
`[muxer] Found ${clips.length} clips. Base timestamp: ${globalStartTime}`,
);
const command = ffmpeg();
const filterParts: string[] = [];
console.log(`[muxer] Creating audio filters for ${clips.length} clips...`);
clips.forEach((clip, index) => {
command.input(clip.oggPath);
// Calculate delay relative to the global start time
const delayMs = clip.meta.startTime - globalStartTime;
@@ -148,27 +145,25 @@ async function startMuxing() {
);
const outputFilename = path.join(recordingsDir, `muxed-${Date.now()}.mp3`);
const inputs = clips.map((clip) => clip.oggPath);
console.log(`[muxer] Combining clips. This might take a while...`);
// Using fluent-ffmpeg's complexFilter
command
.complexFilter(filterParts, "out")
.audioCodec("libmp3lame")
.save(outputFilename)
.on("progress", (progress) => {
if (progress.percent) {
console.log(`[muxer] Progress: ${progress.percent.toFixed(2)}%`);
}
})
.on("end", () => {
console.log(
`[muxer] Successfully muxed! Output saved to: ${outputFilename}`,
);
})
.on("error", (err) => {
console.error(`[muxer] FFmpeg Error:`, err);
try {
const args = buildMuxFfmpegArgs({
inputs,
filter: filterParts.join(";"),
output: outputFilename,
codec: "libmp3lame",
});
await runFfmpeg(args);
console.log(
`[muxer] Successfully muxed! Output saved to: ${outputFilename}`,
);
} catch (err) {
console.error(`[muxer] FFmpeg Error:`, err);
}
}
startMuxing();
+1 -1
View File
@@ -32,7 +32,7 @@ export async function retryWithBackoff<T>(
{
attempt: error.attemptNumber,
retriesLeft: error.retriesLeft,
error: error.message,
error: error.error.message,
},
"Retry attempt",
);
+1 -1
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
export const userStateUpdateSchema = z.object({
const userStateUpdateSchema = z.object({
userId: z.string(),
username: z.string(),
avatar: z.string(),