84 lines
2.6 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|