import { readFile, writeFile, rename } from 'node:fs/promises'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const API = 'https://api.omnapi.com'; const TERMINAL = new Set(['COMPLETED', 'FAILED', 'CANCELLED']); type Json = Record; export type Job = { requestId: string; input: Json; taskId?: string; generationId?: string; result?: Json }; type Request = (path: string, input?: Json, requestId?: string) => Promise; /** Persistence is awaited before any paid create. A timeout retains the task ID. */ export async function runSongJob({ job, save, request, now = Date.now, sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)), timeoutMs = 45 * 60_000 }: { job: Job; save: (job: Job) => Promise; request: Request; now?: () => number; sleep?: (ms: number) => Promise; timeoutMs?: number; }): Promise { if (!job.taskId) { await save(job); const receipt = await request('/api/v1/suno/songs', job.input, job.requestId); if (typeof receipt.taskId !== 'string' || !receipt.taskId) throw new Error('Create receipt has no taskId; retain the request ID and investigate.'); job.taskId = receipt.taskId; job.generationId = typeof receipt.generationId === 'string' ? receipt.generationId : undefined; await save(job); } const deadline = now() + timeoutMs; while (now() < deadline) { const task = await request(`/api/v1/tasks/${encodeURIComponent(job.taskId)}`); if (typeof task.status !== 'string') throw new Error('Invalid task response; retain this job file.'); if (TERMINAL.has(task.status)) { job.result = task; await save(job); return task; } await sleep(Math.min(5000, Math.max(0, deadline - now()))); } throw new Error('Polling deadline reached. Run again with the same job file to resume; no new song is needed.'); } async function main() { const apiKey = process.env.OMNAPI_KEY; if (!apiKey) throw new Error('Set OMNAPI_KEY on this server.'); const filename = resolve(process.argv[2] ?? 'suno-job.json'); let job: Job; try { job = JSON.parse(await readFile(filename, 'utf8')); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; job = { requestId: crypto.randomUUID(), input: { mode: 'simple', model: 'chirp-hawk', prompt: process.env.OMNAPI_PROMPT ?? 'upbeat city pop, bright guitars, summer night drive' }, }; // Exclusive creation avoids replacing another process's persisted request ID. await writeFile(filename, JSON.stringify(job, null, 2), { mode: 0o600, flag: 'wx' }); } if (typeof job.requestId !== 'string' || !job.requestId || !job.input || typeof job.input !== 'object') throw new Error('Invalid job file. Do not overwrite an uncertain paid request.'); const save = async (state: Job) => { const temporary = `${filename}.${crypto.randomUUID()}.tmp`; await writeFile(temporary, JSON.stringify(state, null, 2), { mode: 0o600, flag: 'wx' }); await rename(temporary, filename); }; const request: Request = async (path, input, requestId) => { const response = await fetch(`${API}${path}`, { method: input ? 'POST' : 'GET', headers: { 'x-api-key': apiKey, ...(input ? { 'Content-Type': 'application/json', 'Idempotency-Key': requestId! } : {}) }, body: input ? JSON.stringify(input) : undefined, signal: AbortSignal.timeout(30_000), }); if (!response.ok) { // Never automatically retry a paid write or print response bodies/secrets. const retry = response.headers.get('Retry-After'); throw new Error(`HTTP ${response.status}${retry ? '; observe Retry-After before resuming' : ''}. Retain the same job file and request ID.`); } return await response.json() as Json; }; const result = await runSongJob({ job, save, request }); console.log(`Task ${job.taskId}: ${result.status}. Result saved in the private job file.`); if (result.status !== 'COMPLETED') process.exitCode = 1; } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { main().catch(error => { console.error(error instanceof Error ? error.message : 'Unable to complete the job.'); process.exitCode = 1; }); }