openreader/src/app/api/audiobook/status/route.ts

84 lines
2.6 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { existsSync } from 'fs';
import { join } from 'path';
import { AUDIOBOOKS_V1_DIR, isAudiobooksV1Ready } from '@/lib/server/docstore';
import { listStoredChapters } from '@/lib/server/audiobook';
import type { AudiobookGenerationSettings } from '@/types/client';
import type { TTSAudiobookFormat, TTSAudiobookChapter } from '@/types/tts';
import { readFile } from 'fs/promises';
export const dynamic = 'force-dynamic';
function getAudiobooksRootDir(request: NextRequest): string {
const raw = request.headers.get('x-openreader-test-namespace')?.trim();
if (!raw) return AUDIOBOOKS_V1_DIR;
const safe = raw.replace(/[^a-zA-Z0-9._-]/g, '');
return safe ? join(AUDIOBOOKS_V1_DIR, safe) : AUDIOBOOKS_V1_DIR;
}
const SAFE_ID_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
function isSafeId(value: string): boolean {
return SAFE_ID_REGEX.test(value);
}
export async function GET(request: NextRequest) {
try {
const bookId = request.nextUrl.searchParams.get('bookId');
if (!bookId || !isSafeId(bookId)) {
return NextResponse.json({ error: 'Missing bookId parameter' }, { status: 400 });
}
if (!(await isAudiobooksV1Ready())) {
return NextResponse.json(
{ error: 'Audiobooks storage is not migrated; run /api/migrations/v1 first.' },
{ status: 409 },
);
}
const intermediateDir = join(getAudiobooksRootDir(request), `${bookId}-audiobook`);
if (!existsSync(intermediateDir)) {
return NextResponse.json({
chapters: [],
exists: false,
hasComplete: false,
bookId: null,
settings: null,
});
}
const stored = await listStoredChapters(intermediateDir, request.signal);
const chapters: TTSAudiobookChapter[] = stored.map((chapter) => ({
index: chapter.index,
title: chapter.title,
duration: chapter.durationSec,
status: 'completed',
bookId,
format: chapter.format as TTSAudiobookFormat,
}));
let settings: AudiobookGenerationSettings | null = null;
try {
settings = JSON.parse(await readFile(join(intermediateDir, 'audiobook.meta.json'), 'utf8')) as AudiobookGenerationSettings;
} catch {
settings = null;
}
const hasComplete = existsSync(join(intermediateDir, 'complete.mp3')) || existsSync(join(intermediateDir, 'complete.m4b'));
return NextResponse.json({
chapters,
exists: true,
hasComplete,
bookId,
settings,
});
} catch (error) {
console.error('Error fetching chapters:', error);
return NextResponse.json(
{ error: 'Failed to fetch chapters' },
{ status: 500 }
);
}
}