Merge branch 'worktree-library-modernization'
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user