refactor(api): move audiobook endpoints to new path

- Relocated all audiobook-related API routes from `/api/audio/convert/*` to `/api/audiobook/*`.
- This change affects endpoints for chapter conversion, retrieval, deletion, and overall audiobook status.
- Updated client-side calls in `AudiobookExportModal.tsx`, `EPUBContext.tsx`, and `PDFContext.tsx` to reflect the new API paths.
- Modified API tests (`api.spec.ts`, `export.spec.ts`) to target the restructured endpoints.
- The new API structure provides better organization and a clearer, more consistent interface for audiobook functionality.
This commit is contained in:
Richard Roberson 2025-11-21 19:29:49 -07:00
parent 733b4180eb
commit 7a29f73d07
8 changed files with 44 additions and 43 deletions

View file

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { spawn } from 'child_process';
import { writeFile, readFile, mkdir, unlink, readdir } from 'fs/promises';
import { writeFile, readFile, mkdir, unlink, readdir, rm } from 'fs/promises';
import { existsSync, createReadStream } from 'fs';
import { join } from 'path';
import { randomUUID } from 'crypto';
@ -378,4 +378,31 @@ function streamFile(filePath: string, format: string) {
'Cache-Control': 'no-cache',
},
});
}
export async function DELETE(request: NextRequest) {
try {
const bookId = request.nextUrl.searchParams.get('bookId');
if (!bookId) {
return NextResponse.json({ error: 'Missing bookId parameter' }, { status: 400 });
}
const docstoreDir = join(process.cwd(), 'docstore');
const intermediateDir = join(docstoreDir, `${bookId}-audiobook`);
// If directory doesn't exist, consider it already reset
if (!existsSync(intermediateDir)) {
return NextResponse.json({ success: true, existed: false });
}
// Recursively delete the entire audiobook directory
await rm(intermediateDir, { recursive: true, force: true });
return NextResponse.json({ success: true, existed: true });
} catch (error) {
console.error('Error resetting audiobook:', error);
return NextResponse.json(
{ error: 'Failed to reset audiobook' },
{ status: 500 }
);
}
}

View file

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { readdir, readFile, rm } from 'fs/promises';
import { readdir, readFile } from 'fs/promises';
import { existsSync } from 'fs';
import { join } from 'path';
@ -69,30 +69,4 @@ export async function GET(request: NextRequest) {
}
}
export async function DELETE(request: NextRequest) {
try {
const bookId = request.nextUrl.searchParams.get('bookId');
if (!bookId) {
return NextResponse.json({ error: 'Missing bookId parameter' }, { status: 400 });
}
const docstoreDir = join(process.cwd(), 'docstore');
const intermediateDir = join(docstoreDir, `${bookId}-audiobook`);
// If directory doesn't exist, consider it already reset
if (!existsSync(intermediateDir)) {
return NextResponse.json({ success: true, existed: false });
}
// Recursively delete the entire audiobook directory
await rm(intermediateDir, { recursive: true, force: true });
return NextResponse.json({ success: true, existed: true });
} catch (error) {
console.error('Error resetting audiobook:', error);
return NextResponse.json(
{ error: 'Failed to reset audiobook' },
{ status: 500 }
);
}
}

View file

@ -61,7 +61,7 @@ export function AudiobookExportModal({
setIsLoadingExisting(true);
}
try {
const response = await fetch(`/api/audio/convert/chapters?bookId=${documentId}`);
const response = await fetch(`/api/audiobook/status?bookId=${documentId}`);
if (response.ok) {
const data = await response.json();
if (data.exists && data.chapters.length > 0) {
@ -241,7 +241,7 @@ export function AudiobookExportModal({
const performDeleteChapter = useCallback(async () => {
if (!bookId || !pendingDeleteChapter) return;
try {
const response = await fetch(`/api/audio/convert/chapter?bookId=${bookId}&chapterIndex=${pendingDeleteChapter.index}`, {
const response = await fetch(`/api/audiobook/chapter?bookId=${bookId}&chapterIndex=${pendingDeleteChapter.index}`, {
method: 'DELETE'
});
if (!response.ok) {
@ -261,7 +261,7 @@ export function AudiobookExportModal({
const targetBookId = bookId || documentId;
if (!targetBookId) return;
try {
const resp = await fetch(`/api/audio/convert/chapters?bookId=${targetBookId}`, { method: 'DELETE' });
const resp = await fetch(`/api/audiobook?bookId=${targetBookId}`, { method: 'DELETE' });
if (!resp.ok) {
throw new Error('Reset failed');
}
@ -281,7 +281,7 @@ export function AudiobookExportModal({
if (!chapter.bookId) return;
try {
const response = await fetch(`/api/audio/convert/chapter?bookId=${chapter.bookId}&chapterIndex=${chapter.index}`);
const response = await fetch(`/api/audiobook/chapter?bookId=${chapter.bookId}&chapterIndex=${chapter.index}`);
if (!response.ok) throw new Error('Download failed');
const blob = await response.blob();
@ -306,7 +306,7 @@ export function AudiobookExportModal({
setIsCombining(true);
try {
const response = await fetch(`/api/audio/convert?bookId=${bookId}&format=${format}`);
const response = await fetch(`/api/audiobook?bookId=${bookId}&format=${format}`);
if (!response.ok) throw new Error('Download failed');
const reader = response.body?.getReader();

View file

@ -339,7 +339,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
const existingIndices = new Set<number>();
if (bookId) {
try {
const existingResponse = await fetch(`/api/audio/convert/chapters?bookId=${bookId}`);
const existingResponse = await fetch(`/api/audiobook/status?bookId=${bookId}`);
if (existingResponse.ok) {
const existingData = await existingResponse.json();
if (existingData.chapters && existingData.chapters.length > 0) {
@ -469,7 +469,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
}
// Send to server for conversion and storage
const convertResponse = await fetch('/api/audio/convert', {
const convertResponse = await fetch(`/api/audiobook`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -645,7 +645,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
}
// Send to server for conversion and storage
const convertResponse = await fetch('/api/audio/convert', {
const convertResponse = await fetch('/api/audiobook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -299,7 +299,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
const existingIndices = new Set<number>();
if (bookId) {
try {
const existingResponse = await fetch(`/api/audio/convert/chapters?bookId=${bookId}`);
const existingResponse = await fetch(`/api/audiobook/status?bookId=${bookId}`);
if (existingResponse.ok) {
const existingData = await existingResponse.json();
if (existingData.chapters && existingData.chapters.length > 0) {
@ -399,7 +399,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
}
// Send to server for conversion and storage
const convertResponse = await fetch('/api/audio/convert', {
const convertResponse = await fetch(`/api/audiobook`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -587,7 +587,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
}
// Send to server for conversion and storage
const convertResponse = await fetch('/api/audio/convert', {
const convertResponse = await fetch('/api/audiobook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -9,9 +9,9 @@ test.describe('API health checks', () => {
expect(json.voices.length).toBeGreaterThan(0);
});
test('GET /api/audio/convert/chapters returns 200 with exists flag and chapters array', async ({ request }) => {
test('GET /api/audiobook/status returns 200 with exists flag and chapters array', async ({ request }) => {
const bookId = `healthcheck-${Date.now()}`;
const res = await request.get(`/api/audio/convert/chapters?bookId=${bookId}`);
const res = await request.get(`/api/audiobook/status?bookId=${bookId}`);
expect(res.ok()).toBeTruthy();
const json = await res.json();
expect(json).toHaveProperty('exists');

View file

@ -67,7 +67,7 @@ async function getAudioDurationSeconds(filePath: string) {
}
async function expectChaptersBackendState(page: Page, bookId: string) {
const res = await page.request.get(`/api/audio/convert/chapters?bookId=${bookId}`);
const res = await page.request.get(`/api/audiobook/status?bookId=${bookId}`);
expect(res.ok()).toBeTruthy();
const json = await res.json();
return json;
@ -271,7 +271,7 @@ test.describe('Audiobook export', () => {
).toBeVisible({ timeout: 60_000 });
// Backend should report no existing chapters for this bookId
const res = await page.request.get(`/api/audio/convert/chapters?bookId=${bookId}`);
const res = await page.request.get(`/api/audiobook/status?bookId=${bookId}`);
expect(res.ok()).toBeTruthy();
const json = await res.json();
expect(json.exists).toBe(false);