Add server: Express web UI + API for remote audio description generation with job queue, basic auth, resumable processing, subtitles, and muxing
This commit is contained in:
59
src/server/db/index.ts
Normal file
59
src/server/db/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const DB_PATH = path.resolve('./data/server.db');
|
||||
|
||||
let db: Database.Database;
|
||||
|
||||
export function getDb(): Database.Database {
|
||||
if (!db) {
|
||||
const dir = path.dirname(DB_PATH);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
db = new Database(DB_PATH);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
migrate();
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
function migrate(): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
video_path TEXT NOT NULL,
|
||||
video_filename TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
config TEXT NOT NULL,
|
||||
progress REAL DEFAULT 0,
|
||||
current_index INTEGER DEFAULT 0,
|
||||
total_units INTEGER DEFAULT 0,
|
||||
segments TEXT DEFAULT '[]',
|
||||
last_context TEXT DEFAULT '{}',
|
||||
current_time_position REAL DEFAULT 0,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
completed_at TEXT,
|
||||
output_audio TEXT,
|
||||
output_subtitles_srt TEXT,
|
||||
output_subtitles_vtt TEXT,
|
||||
output_muxed TEXT,
|
||||
output_options TEXT DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
export function closeDb(): void {
|
||||
if (db) {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
123
src/server/db/jobStore.ts
Normal file
123
src/server/db/jobStore.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { getDb } from '../db';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface OutputOptions {
|
||||
audio: boolean;
|
||||
subtitles: boolean;
|
||||
muxed: boolean;
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: string;
|
||||
video_path: string;
|
||||
video_filename: string;
|
||||
status: 'pending' | 'queued' | 'processing' | 'paused' | 'completed' | 'failed' | 'cancelled';
|
||||
config: string;
|
||||
progress: number;
|
||||
current_index: number;
|
||||
total_units: number;
|
||||
segments: string;
|
||||
last_context: string;
|
||||
current_time_position: number;
|
||||
error: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
completed_at: string | null;
|
||||
output_audio: string | null;
|
||||
output_subtitles_srt: string | null;
|
||||
output_subtitles_vtt: string | null;
|
||||
output_muxed: string | null;
|
||||
output_options: string;
|
||||
}
|
||||
|
||||
export function getAllJobs(): Job[] {
|
||||
const db = getDb();
|
||||
return db.prepare('SELECT * FROM jobs ORDER BY created_at DESC').all() as Job[];
|
||||
}
|
||||
|
||||
export function getJob(id: string): Job | undefined {
|
||||
const db = getDb();
|
||||
return db.prepare('SELECT * FROM jobs WHERE id = ?').get(id) as Job | undefined;
|
||||
}
|
||||
|
||||
export function createJob(videoPath: string, filename: string, config: object, outputOptions: OutputOptions): Job {
|
||||
const db = getDb();
|
||||
const id = uuidv4();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO jobs (id, video_path, video_filename, config, output_options, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, videoPath, filename, JSON.stringify(config), JSON.stringify(outputOptions), now, now);
|
||||
|
||||
return getJob(id)!;
|
||||
}
|
||||
|
||||
export function updateJobStatus(id: string, status: Job['status'], error?: string): void {
|
||||
const db = getDb();
|
||||
const now = new Date().toISOString();
|
||||
const completedAt = status === 'completed' ? now : null;
|
||||
db.prepare(`
|
||||
UPDATE jobs SET status = ?, error = ?, updated_at = ?, completed_at = ? WHERE id = ?
|
||||
`).run(status, error || null, now, completedAt, id);
|
||||
}
|
||||
|
||||
export function saveCheckpoint(
|
||||
id: string,
|
||||
segments: string,
|
||||
currentIndex: number,
|
||||
totalUnits: number,
|
||||
currentTimePosition: number,
|
||||
lastContext: string,
|
||||
progress: number
|
||||
): void {
|
||||
const db = getDb();
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(`
|
||||
UPDATE jobs SET segments = ?, current_index = ?, total_units = ?, current_time_position = ?, last_context = ?, progress = ?, updated_at = ? WHERE id = ?
|
||||
`).run(segments, currentIndex, totalUnits, currentTimePosition, lastContext, progress, now, id);
|
||||
}
|
||||
|
||||
export function saveJobOutputs(
|
||||
id: string,
|
||||
outputs: { audio?: string; subtitlesSrt?: string; subtitlesVtt?: string; muxed?: string }
|
||||
): void {
|
||||
const db = getDb();
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(`
|
||||
UPDATE jobs SET output_audio = ?, output_subtitles_srt = ?, output_subtitles_vtt = ?, output_muxed = ?, updated_at = ? WHERE id = ?
|
||||
`).run(
|
||||
outputs.audio || null,
|
||||
outputs.subtitlesSrt || null,
|
||||
outputs.subtitlesVtt || null,
|
||||
outputs.muxed || null,
|
||||
now,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteJob(id: string): void {
|
||||
const db = getDb();
|
||||
db.prepare('DELETE FROM jobs WHERE id = ?').run(id);
|
||||
}
|
||||
|
||||
export function getConfigValue(key: string): string | undefined {
|
||||
const db = getDb();
|
||||
const row = db.prepare('SELECT value FROM config WHERE key = ?').get(key) as { value: string } | undefined;
|
||||
return row?.value;
|
||||
}
|
||||
|
||||
export function setConfigValue(key: string, value: string): void {
|
||||
const db = getDb();
|
||||
db.prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)').run(key, value);
|
||||
}
|
||||
|
||||
export function getAllConfig(): Record<string, string> {
|
||||
const db = getDb();
|
||||
const rows = db.prepare('SELECT key, value FROM config').all() as { key: string; value: string }[];
|
||||
const config: Record<string, string> = {};
|
||||
for (const row of rows) {
|
||||
config[row.key] = row.value;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
Reference in New Issue
Block a user