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:
parent
733b4180eb
commit
7a29f73d07
8 changed files with 44 additions and 43 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { spawn } from 'child_process';
|
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 { existsSync, createReadStream } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
|
@ -379,3 +379,30 @@ function streamFile(filePath: string, format: string) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { readdir, readFile, rm } from 'fs/promises';
|
import { readdir, readFile } from 'fs/promises';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { join } from 'path';
|
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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -61,7 +61,7 @@ export function AudiobookExportModal({
|
||||||
setIsLoadingExisting(true);
|
setIsLoadingExisting(true);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/audio/convert/chapters?bookId=${documentId}`);
|
const response = await fetch(`/api/audiobook/status?bookId=${documentId}`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.exists && data.chapters.length > 0) {
|
if (data.exists && data.chapters.length > 0) {
|
||||||
|
|
@ -241,7 +241,7 @@ export function AudiobookExportModal({
|
||||||
const performDeleteChapter = useCallback(async () => {
|
const performDeleteChapter = useCallback(async () => {
|
||||||
if (!bookId || !pendingDeleteChapter) return;
|
if (!bookId || !pendingDeleteChapter) return;
|
||||||
try {
|
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'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -261,7 +261,7 @@ export function AudiobookExportModal({
|
||||||
const targetBookId = bookId || documentId;
|
const targetBookId = bookId || documentId;
|
||||||
if (!targetBookId) return;
|
if (!targetBookId) return;
|
||||||
try {
|
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) {
|
if (!resp.ok) {
|
||||||
throw new Error('Reset failed');
|
throw new Error('Reset failed');
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +281,7 @@ export function AudiobookExportModal({
|
||||||
if (!chapter.bookId) return;
|
if (!chapter.bookId) return;
|
||||||
|
|
||||||
try {
|
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');
|
if (!response.ok) throw new Error('Download failed');
|
||||||
|
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
|
|
@ -306,7 +306,7 @@ export function AudiobookExportModal({
|
||||||
|
|
||||||
setIsCombining(true);
|
setIsCombining(true);
|
||||||
try {
|
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');
|
if (!response.ok) throw new Error('Download failed');
|
||||||
|
|
||||||
const reader = response.body?.getReader();
|
const reader = response.body?.getReader();
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
const existingIndices = new Set<number>();
|
const existingIndices = new Set<number>();
|
||||||
if (bookId) {
|
if (bookId) {
|
||||||
try {
|
try {
|
||||||
const existingResponse = await fetch(`/api/audio/convert/chapters?bookId=${bookId}`);
|
const existingResponse = await fetch(`/api/audiobook/status?bookId=${bookId}`);
|
||||||
if (existingResponse.ok) {
|
if (existingResponse.ok) {
|
||||||
const existingData = await existingResponse.json();
|
const existingData = await existingResponse.json();
|
||||||
if (existingData.chapters && existingData.chapters.length > 0) {
|
if (existingData.chapters && existingData.chapters.length > 0) {
|
||||||
|
|
@ -469,7 +469,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to server for conversion and storage
|
// Send to server for conversion and storage
|
||||||
const convertResponse = await fetch('/api/audio/convert', {
|
const convertResponse = await fetch(`/api/audiobook`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -645,7 +645,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to server for conversion and storage
|
// Send to server for conversion and storage
|
||||||
const convertResponse = await fetch('/api/audio/convert', {
|
const convertResponse = await fetch('/api/audiobook', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
const existingIndices = new Set<number>();
|
const existingIndices = new Set<number>();
|
||||||
if (bookId) {
|
if (bookId) {
|
||||||
try {
|
try {
|
||||||
const existingResponse = await fetch(`/api/audio/convert/chapters?bookId=${bookId}`);
|
const existingResponse = await fetch(`/api/audiobook/status?bookId=${bookId}`);
|
||||||
if (existingResponse.ok) {
|
if (existingResponse.ok) {
|
||||||
const existingData = await existingResponse.json();
|
const existingData = await existingResponse.json();
|
||||||
if (existingData.chapters && existingData.chapters.length > 0) {
|
if (existingData.chapters && existingData.chapters.length > 0) {
|
||||||
|
|
@ -399,7 +399,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to server for conversion and storage
|
// Send to server for conversion and storage
|
||||||
const convertResponse = await fetch('/api/audio/convert', {
|
const convertResponse = await fetch(`/api/audiobook`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -587,7 +587,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to server for conversion and storage
|
// Send to server for conversion and storage
|
||||||
const convertResponse = await fetch('/api/audio/convert', {
|
const convertResponse = await fetch('/api/audiobook', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ test.describe('API health checks', () => {
|
||||||
expect(json.voices.length).toBeGreaterThan(0);
|
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 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();
|
expect(res.ok()).toBeTruthy();
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
expect(json).toHaveProperty('exists');
|
expect(json).toHaveProperty('exists');
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ async function getAudioDurationSeconds(filePath: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expectChaptersBackendState(page: Page, bookId: 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();
|
expect(res.ok()).toBeTruthy();
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
return json;
|
return json;
|
||||||
|
|
@ -271,7 +271,7 @@ test.describe('Audiobook export', () => {
|
||||||
).toBeVisible({ timeout: 60_000 });
|
).toBeVisible({ timeout: 60_000 });
|
||||||
|
|
||||||
// Backend should report no existing chapters for this bookId
|
// 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();
|
expect(res.ok()).toBeTruthy();
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
expect(json.exists).toBe(false);
|
expect(json.exists).toBe(false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue