mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
cb9f1b81bc
Introduce full support for Audible series exploration: API routes, frontend pages, components, hooks, and integrations. Key changes: - Prisma: add Audiobook.seriesAsin for linking audiobooks to series detail pages. - Backend: add /api/series/search and /api/series/[asin] routes that require auth; scrape Audible series data and enrich books with library availability. - Integrations/services: add audible-series integration and update request/HTTP services to support the workflow. - Frontend: add /series and /series/[asin] pages, new components (SeriesCard, SeriesGrid, SeriesDetailCard, SimilarSeriesRow) and wire them to a new useSeries hook; update AudiobookDetailsModal to show/link series; add Series link to Header. - Misc: extend audiobook types with series fields and add seriesLabels to language-config for scraping. These changes enable users to search for series, view series metadata and books, and navigate between audiobook and series detail pages.
73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
/**
|
|
* Component: Series Detail API Route
|
|
* Documentation: documentation/integrations/audible.md
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getCurrentUser } from '@/lib/middleware/auth';
|
|
import { RMABLogger } from '@/lib/utils/logger';
|
|
import { scrapeSeriesPage } from '@/lib/integrations/audible-series';
|
|
import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher';
|
|
|
|
const logger = RMABLogger.create('API.Series.Detail');
|
|
|
|
/**
|
|
* GET /api/series/{asin}
|
|
* Fetch series detail: metadata + books (enriched with availability) + similar series
|
|
*/
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ asin: string }> }
|
|
) {
|
|
try {
|
|
const currentUser = getCurrentUser(request);
|
|
if (!currentUser) {
|
|
return NextResponse.json(
|
|
{ error: 'Unauthorized', message: 'Authentication required' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const { asin } = await params;
|
|
|
|
if (!asin || !/^[A-Z0-9]{10}$/.test(asin)) {
|
|
return NextResponse.json(
|
|
{ error: 'ValidationError', message: 'Valid series ASIN is required' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
logger.info(`Fetching series detail: ${asin}`);
|
|
|
|
const detail = await scrapeSeriesPage(asin);
|
|
if (!detail) {
|
|
return NextResponse.json(
|
|
{ error: 'NotFound', message: 'Series not found' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Enrich books with library availability and request status
|
|
const userId = currentUser.sub || undefined;
|
|
const enrichedBooks = await enrichAudiobooksWithMatches(detail.books, userId);
|
|
|
|
logger.info(`Series detail complete: "${detail.title}" (${enrichedBooks.length} books)`);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
series: {
|
|
...detail,
|
|
books: enrichedBooks,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error('Failed to fetch series detail', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
return NextResponse.json(
|
|
{ error: 'FetchError', message: 'Failed to fetch series details' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|