mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-18 04:00:10 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* BookDate: Force Generate New Recommendations
|
||||
* Documentation: documentation/features/bookdate-prd.md
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import {
|
||||
buildAIPrompt,
|
||||
callAI,
|
||||
matchToAudnexus,
|
||||
isInLibrary,
|
||||
isAlreadyRequested,
|
||||
isAlreadySwiped,
|
||||
} from '@/lib/bookdate/helpers';
|
||||
|
||||
async function handler(req: AuthenticatedRequest) {
|
||||
try {
|
||||
const userId = req.user!.id;
|
||||
|
||||
// Get global config
|
||||
const config = await prisma.bookDateConfig.findFirst();
|
||||
|
||||
if (!config || !config.isVerified || !config.isEnabled) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'BookDate is not configured or has been disabled. Please contact your administrator.',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get user's preferences
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
bookDateLibraryScope: true,
|
||||
bookDateCustomPrompt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build user preferences object
|
||||
const userPreferences = {
|
||||
libraryScope: user.bookDateLibraryScope || 'full',
|
||||
customPrompt: user.bookDateCustomPrompt || null,
|
||||
};
|
||||
|
||||
// Build prompt and call AI (same as recommendations endpoint, but doesn't check cache)
|
||||
console.log('[BookDate] Force generating new recommendations for user:', userId);
|
||||
const prompt = await buildAIPrompt(userId, userPreferences);
|
||||
const aiResponse = await callAI(config.provider, config.model, config.apiKey, prompt);
|
||||
|
||||
if (!aiResponse.recommendations || !Array.isArray(aiResponse.recommendations)) {
|
||||
throw new Error('Invalid AI response format: missing recommendations array');
|
||||
}
|
||||
|
||||
console.log(`[BookDate] AI returned ${aiResponse.recommendations.length} recommendations`);
|
||||
|
||||
// Match to Audnexus and filter
|
||||
const batchId = `batch_${Date.now()}`;
|
||||
const matched: any[] = [];
|
||||
|
||||
for (const rec of aiResponse.recommendations) {
|
||||
if (!rec.title || !rec.author) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already swiped
|
||||
if (await isAlreadySwiped(userId, rec.title, rec.author)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if in library
|
||||
if (await isInLibrary(userId, rec.title, rec.author)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match to Audnexus
|
||||
try {
|
||||
const audnexusMatch = await matchToAudnexus(rec.title, rec.author);
|
||||
|
||||
if (!audnexusMatch) {
|
||||
console.warn(`[BookDate] No Audnexus match: "${rec.title}" by ${rec.author}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check again if in library with ASIN for exact matching
|
||||
// This catches books that might have different titles (e.g., "The Tenant" vs "The Tenant (Unabridged)")
|
||||
if (await isInLibrary(userId, audnexusMatch.title, audnexusMatch.author, audnexusMatch.asin)) {
|
||||
console.log(`[BookDate] Book "${audnexusMatch.title}" (ASIN: ${audnexusMatch.asin}) is in library, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already requested
|
||||
if (await isAlreadyRequested(userId, audnexusMatch.asin)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matched.push({
|
||||
userId,
|
||||
batchId,
|
||||
title: audnexusMatch.title,
|
||||
author: audnexusMatch.author,
|
||||
narrator: audnexusMatch.narrator,
|
||||
rating: audnexusMatch.rating,
|
||||
description: audnexusMatch.description,
|
||||
coverUrl: audnexusMatch.coverUrl,
|
||||
audnexusAsin: audnexusMatch.asin,
|
||||
aiReason: rec.reason || 'Recommended based on your preferences',
|
||||
});
|
||||
|
||||
if (matched.length >= 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`[BookDate] Match error for "${rec.title}":`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[BookDate] Matched ${matched.length} new recommendations`);
|
||||
|
||||
if (matched.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Could not find any new recommendations. Try adjusting your settings or check back later.',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Save to database
|
||||
await prisma.bookDateRecommendation.createMany({
|
||||
data: matched,
|
||||
});
|
||||
|
||||
// Return all cached recommendations (excluding swiped ones)
|
||||
const allRecommendations = await prisma.bookDateRecommendation.findMany({
|
||||
where: {
|
||||
userId,
|
||||
// Exclude recommendations that have associated swipes
|
||||
swipes: {
|
||||
none: {},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
take: 10,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
recommendations: allRecommendations,
|
||||
source: 'generated',
|
||||
generatedCount: matched.length,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[BookDate] Generate error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.message || 'Failed to generate new recommendations',
|
||||
details: process.env.NODE_ENV === 'development' ? error.stack : undefined,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return requireAuth(req, handler);
|
||||
}
|
||||
Reference in New Issue
Block a user