/** * Component: New Releases API Route * Documentation: documentation/integrations/audible.md * * Serves new release audiobooks from AudibleCacheCategory with real-time library matching. * New releases are stored with categoryId '__new_releases__' in the unified category table. */ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db'; import { enrichAudiobooksWithMatches, getAvailableAsins } from '@/lib/utils/audiobook-matcher'; import { getCurrentUser } from '@/lib/middleware/auth'; import { RMABLogger } from '@/lib/utils/logger'; import { NEW_RELEASES_CATEGORY_ID } from '@/lib/processors/audible-refresh.processor'; const logger = RMABLogger.create('API.Audiobooks.NewReleases'); /** * GET /api/audiobooks/new-releases?page=1&limit=20 * Get new release audiobooks from AudibleCacheCategory with pagination * * Real-time matching against plex_library determines availability. */ export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams; const page = parseInt(searchParams.get('page') || '1', 10); const limit = parseInt(searchParams.get('limit') || '20', 10); const hideAvailable = searchParams.get('hideAvailable') === 'true'; // Validate pagination parameters if (page < 1 || limit < 1 || limit > 100) { return NextResponse.json( { error: 'ValidationError', message: 'Invalid pagination parameters. Page must be >= 1 and limit must be between 1 and 100.', }, { status: 400 } ); } const skip = (page - 1) * limit; // When hideAvailable is enabled, exclude ASINs that are in the library or have completed requests let excludedAsins: string[] = []; if (hideAvailable) { const availableSet = await getAvailableAsins(); excludedAsins = [...availableSet]; } const whereClause: any = { categoryId: NEW_RELEASES_CATEGORY_ID }; if (excludedAsins.length > 0) { whereClause.asin = { notIn: excludedAsins }; } // Query AudibleCacheCategory for new release audiobooks const [categoryEntries, totalCount] = await Promise.all([ prisma.audibleCacheCategory.findMany({ where: whereClause, orderBy: { rank: 'asc' }, skip, take: limit, select: { asin: true, rank: true }, }), prisma.audibleCacheCategory.count({ where: whereClause }), ]); // If no data found, return helpful message if (totalCount === 0) { return NextResponse.json({ success: true, audiobooks: [], count: 0, totalCount: 0, page, totalPages: 0, hasMore: false, message: 'No new releases found. The Audible data refresh job may need to be run. Please check the Admin Jobs page to enable or trigger the "Audible Data Refresh" job.', }); } // Fetch full metadata from AudibleCache for these ASINs const asins = categoryEntries.map((e) => e.asin); const cacheEntries = await prisma.audibleCache.findMany({ where: { asin: { in: asins } }, select: { asin: true, title: true, author: true, narrator: true, description: true, coverArtUrl: true, cachedCoverPath: true, durationMinutes: true, releaseDate: true, rating: true, genres: true, lastSyncedAt: true, }, }); // Build a map for ordering by rank const cacheMap = new Map(cacheEntries.map((e) => [e.asin, e])); // Transform to matcher input format, preserving rank order const audibleBooks = categoryEntries .map((entry) => { const book = cacheMap.get(entry.asin); if (!book) return null; let coverUrl = book.coverArtUrl || undefined; if (book.cachedCoverPath) { const filename = book.cachedCoverPath.split('/').pop(); coverUrl = `/api/cache/thumbnails/${filename}`; } return { asin: book.asin, title: book.title, author: book.author, narrator: book.narrator || undefined, description: book.description || undefined, coverArtUrl: coverUrl, durationMinutes: book.durationMinutes || undefined, releaseDate: book.releaseDate?.toISOString() || undefined, rating: book.rating ? parseFloat(book.rating.toString()) : undefined, genres: (book.genres as string[]) || [], }; }) .filter(Boolean) as any[]; // Get current user (optional - for request status enrichment) const currentUser = getCurrentUser(request); const userId = currentUser?.sub || undefined; // Enrich with real-time Plex library matching and request status const enrichedAudiobooks = await enrichAudiobooksWithMatches(audibleBooks, userId); const totalPages = Math.ceil(totalCount / limit); const hasMore = page < totalPages; return NextResponse.json({ success: true, audiobooks: enrichedAudiobooks, count: enrichedAudiobooks.length, totalCount, page, totalPages, hasMore, lastSync: cacheEntries[0]?.lastSyncedAt?.toISOString() || null, }); } catch (error) { logger.error('Failed to get new releases', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { error: 'FetchError', message: 'Failed to fetch new releases from database', }, { status: 500 } ); } }