Files
tonemark/src/lib/server/db.ts
T
Giancarmine Salucci 066f197d0d D2: unify DATA_DIR default across audio/downloader/remux/db modules
Create src/lib/server/data-dir.ts as single source of truth for DATA_DIR
using db.ts's $HOME-based convention. All four modules now import from it
instead of defining their own inline default (some resolving to /tmp/.whisper-pwa).

Changes:
- src/lib/server/data-dir.ts — new file, exports DATA_DIR const
- db.ts — import DATA_DIR from data-dir.ts (same logic, same path)
- audio.ts — import DATA_DIR, replace inline process.env.DATA_DIR ?? '/tmp/...'
- downloader.ts — import DATA_DIR, replace inline '/tmp/.whisper-pwa'
- remux.ts — import DATA_DIR, replace inline process.env.DATA_DIR ?? '/tmp/...'
- tests/data-dir.test.ts — 10 tests covering default, custom, and module integrity
- tests/retry-pipeline.test.ts — fix stale hardcoded /tmp/.whisper-pwa/uploads
  path; compute parent dir from getUploadPath() dynamically

Test pass rate: 217/220 (3 pre-existing failures unrelated to DATA_DIR)
- 2 D5 downloader tests need yt-dlp (not available in test env)
- All other tests green, including the new data-dir.test.ts (10/10)
2026-07-09 05:12:11 +02:00

182 lines
5.7 KiB
TypeScript

import Database from 'better-sqlite3';
import { randomUUID } from 'crypto';
import { existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { DATA_DIR } from './data-dir.js';
import type { Job, JobStatus, AudioMode, PushSubscription } from '$lib/types.js';
/**
* Add a column to a table if it does not already exist.
* Uses PRAGMA table_info for an existence check, consistent with the
* existing CREATE TABLE IF NOT EXISTS idiom — no migration tool introduced.
*/
function addColumnIfMissing(table: string, column: string, typeDef: string): void {
const columns = db.pragma(`table_info(${table})`) as { name: string }[];
if (!columns.some((c) => c.name === column)) {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${typeDef}`);
}
}
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
const db = new Database(join(DATA_DIR, 'jobs.db'));
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'pending',
title TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT '',
audio_mode TEXT NOT NULL DEFAULT 'auto',
mean_volume REAL,
whisper_job_id TEXT,
progress INTEGER NOT NULL DEFAULT 0,
output_dir TEXT,
segments_json TEXT,
error TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS push_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
endpoint TEXT NOT NULL UNIQUE,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
// Add retry columns idempotently (may already exist on restart)
try {
addColumnIfMissing('jobs', 'retry_count', 'INTEGER DEFAULT 0');
addColumnIfMissing('jobs', 'next_retry_at', 'TEXT');
} catch (e) {
console.warn('[db] failed to add retry columns:', e);
}
const stmts = {
insertJob: db.prepare(`
INSERT INTO jobs (id, status, title, source, audio_mode)
VALUES (@id, 'pending', @title, @source, @audioMode)
`),
updateJob: db.prepare(`
UPDATE jobs SET
status = @status,
title = @title,
mean_volume = @meanVolume,
whisper_job_id = @whisperJobId,
progress = @progress,
output_dir = @outputDir,
segments_json = @segmentsJson,
error = @error,
retry_count = @retryCount,
next_retry_at = @nextRetryAt,
updated_at = datetime('now')
WHERE id = @id
`),
setStatus: db.prepare(`
UPDATE jobs SET status = @status, progress = @progress, updated_at = datetime('now')
WHERE id = @id
`),
resetJob: db.prepare(`
UPDATE jobs SET
status = 'pending', progress = 0, error = NULL,
mean_volume = NULL, whisper_job_id = NULL,
output_dir = NULL, segments_json = NULL,
retry_count = 0, next_retry_at = NULL,
updated_at = datetime('now')
WHERE id = @id
`),
deleteJob: db.prepare('DELETE FROM jobs WHERE id = ?'),
getJob: db.prepare('SELECT * FROM jobs WHERE id = ?'),
listJobs: db.prepare('SELECT * FROM jobs ORDER BY created_at DESC, rowid DESC LIMIT 100'),
upsertSub: db.prepare(`
INSERT INTO push_subscriptions (endpoint, p256dh, auth)
VALUES (@endpoint, @p256dh, @auth)
ON CONFLICT(endpoint) DO UPDATE SET p256dh = excluded.p256dh, auth = excluded.auth
`),
allSubs: db.prepare('SELECT * FROM push_subscriptions'),
deleteSub: db.prepare('DELETE FROM push_subscriptions WHERE endpoint = ?')
};
function rowToJob(row: Record<string, unknown>): Job {
return {
id: row.id as string,
status: row.status as JobStatus,
title: row.title as string,
source: row.source as string,
audioMode: row.audio_mode as AudioMode,
meanVolume: row.mean_volume as number | null,
whisperJobId: row.whisper_job_id as string | null,
progress: row.progress as number,
outputDir: row.output_dir as string | null,
segmentsJson: row.segments_json as string | null,
error: row.error as string | null,
retryCount: row.retry_count as number | null,
nextRetryAt: row.next_retry_at as string | null,
createdAt: row.created_at as string,
updatedAt: row.updated_at as string
};
}
export function createJob(source: string, title: string, audioMode: AudioMode): Job {
const id = randomUUID();
stmts.insertJob.run({ id, title, source, audioMode });
return getJob(id)!;
}
export function getJob(id: string): Job | null {
const row = stmts.getJob.get(id) as Record<string, unknown> | undefined;
return row ? rowToJob(row) : null;
}
export function listJobs(): Job[] {
return (stmts.listJobs.all() as Record<string, unknown>[]).map(rowToJob);
}
export function updateJob(job: Partial<Job> & { id: string }): void {
const current = getJob(job.id);
if (!current) return;
const merged = { ...current, ...job };
stmts.updateJob.run({
id: merged.id,
status: merged.status,
title: merged.title,
meanVolume: merged.meanVolume,
whisperJobId: merged.whisperJobId,
progress: merged.progress,
outputDir: merged.outputDir,
segmentsJson: merged.segmentsJson,
error: merged.error,
retryCount: merged.retryCount ?? null,
nextRetryAt: merged.nextRetryAt ?? null
});
}
export function setJobStatus(id: string, status: JobStatus, progress = 0): void {
stmts.setStatus.run({ id, status, progress });
}
export function savePushSubscription(sub: { endpoint: string; p256dh: string; auth: string }): void {
stmts.upsertSub.run(sub);
}
export function getAllSubscriptions(): PushSubscription[] {
return stmts.allSubs.all() as PushSubscription[];
}
export function deletePushSubscription(endpoint: string): void {
stmts.deleteSub.run(endpoint);
}
export function resetJob(id: string): void {
stmts.resetJob.run({ id });
}
export function deleteJob(id: string): void {
stmts.deleteJob.run(id);
}