Propagate and use customSearchTerms for ebooks

Persist and apply customSearchTerms across ebook workflows and searches. Updated admin search-terms PATCH to enqueue addSearchEbookJob for ebook requests. Included customSearchTerms when creating ebook request records in audiobooks/[asin]/fetch-ebook, audiobooks/[asin]/select-ebook and requests/[id]/fetch-ebook. Reworked requests/[id]/select-ebook to handle being passed either an audiobook or ebook request (resolve parent audiobook, reuse existing ebook request if present) and to propagate parent.customSearchTerms when creating new ebook requests. Modified search-ebook.processor to read customSearchTerms from the request record, use it as the effective search title (with logging), and pass the modified audiobook title into Anna's Archive and indexer searches so custom terms are honored.
This commit is contained in:
kikootwo
2026-03-05 17:14:26 -05:00
parent f09931f352
commit 137e2b5607
6 changed files with 58 additions and 19 deletions
@@ -100,15 +100,21 @@ export async function PATCH(
}, },
}); });
// Queue search job // Queue search job based on request type
const { getJobQueueService } = await import('@/lib/services/job-queue.service'); const { getJobQueueService } = await import('@/lib/services/job-queue.service');
const jobQueue = getJobQueueService(); const jobQueue = getJobQueueService();
await jobQueue.addSearchJob(id, { const audiobookData = {
id: existingRequest.audiobook.id, id: existingRequest.audiobook.id,
title: existingRequest.audiobook.title, title: existingRequest.audiobook.title,
author: existingRequest.audiobook.author, author: existingRequest.audiobook.author,
asin: existingRequest.audiobook.audibleAsin || undefined, asin: existingRequest.audiobook.audibleAsin || undefined,
}); };
if (existingRequest.type === 'ebook') {
await jobQueue.addSearchEbookJob(id, audiobookData);
} else {
await jobQueue.addSearchJob(id, audiobookData);
}
searchTriggered = true; searchTriggered = true;
logger.info(`Search triggered for request ${id} with custom terms`, { requestId: id }); logger.info(`Search triggered for request ${id} with custom terms`, { requestId: id });
@@ -260,6 +260,7 @@ export async function POST(
parentRequestId: availableRequest?.id || null, // Link to parent if exists parentRequestId: availableRequest?.id || null, // Link to parent if exists
status: 'awaiting_approval', status: 'awaiting_approval',
progress: 0, progress: 0,
customSearchTerms: availableRequest?.customSearchTerms || null,
}, },
}); });
@@ -292,6 +293,7 @@ export async function POST(
parentRequestId: availableRequest?.id || null, parentRequestId: availableRequest?.id || null,
status: 'pending', status: 'pending',
progress: 0, progress: 0,
customSearchTerms: availableRequest?.customSearchTerms || null,
}, },
}); });
@@ -252,6 +252,7 @@ export async function POST(
status: 'awaiting_approval', status: 'awaiting_approval',
progress: 0, progress: 0,
selectedTorrent: selectedEbook as any, selectedTorrent: selectedEbook as any,
customSearchTerms: availableRequest?.customSearchTerms || null,
}, },
}); });
logger.info(`Created ebook request ${ebookRequest.id}, awaiting approval`); logger.info(`Created ebook request ${ebookRequest.id}, awaiting approval`);
@@ -296,6 +297,7 @@ export async function POST(
parentRequestId: availableRequest?.id || null, parentRequestId: availableRequest?.id || null,
status: 'searching', status: 'searching',
progress: 0, progress: 0,
customSearchTerms: availableRequest?.customSearchTerms || null,
}, },
}); });
logger.info(`Created new ebook request ${ebookRequest.id}`); logger.info(`Created new ebook request ${ebookRequest.id}`);
@@ -123,6 +123,7 @@ export async function POST(
parentRequestId, parentRequestId,
status: 'pending', status: 'pending',
progress: 0, progress: 0,
customSearchTerms: parentRequest.customSearchTerms,
}, },
}); });
+31 -12
View File
@@ -52,17 +52,32 @@ export async function POST(
return NextResponse.json({ error: 'Ebook source not specified' }, { status: 400 }); return NextResponse.json({ error: 'Ebook source not specified' }, { status: 400 });
} }
// Get the parent audiobook request // Get the request - could be an audiobook request or an existing ebook request
const parentRequest = await prisma.request.findUnique({ const foundRequest = await prisma.request.findUnique({
where: { id: parentRequestId }, where: { id: parentRequestId },
include: { audiobook: true }, include: { audiobook: true },
}); });
if (!parentRequest) { if (!foundRequest) {
return NextResponse.json({ error: 'Request not found' }, { status: 404 }); return NextResponse.json({ error: 'Request not found' }, { status: 404 });
} }
if (parentRequest.type !== 'audiobook') { // If this is an ebook request, find the parent audiobook request
let parentRequest;
if (foundRequest.type === 'ebook') {
if (!foundRequest.parentRequestId) {
return NextResponse.json({ error: 'Ebook request has no parent audiobook request' }, { status: 400 });
}
parentRequest = await prisma.request.findUnique({
where: { id: foundRequest.parentRequestId },
include: { audiobook: true },
});
if (!parentRequest) {
return NextResponse.json({ error: 'Parent audiobook request not found' }, { status: 404 });
}
} else if (foundRequest.type === 'audiobook') {
parentRequest = foundRequest;
} else {
return NextResponse.json({ error: 'Can only select ebooks for audiobook requests' }, { status: 400 }); return NextResponse.json({ error: 'Can only select ebooks for audiobook requests' }, { status: 400 });
} }
@@ -74,13 +89,16 @@ export async function POST(
} }
// Check for existing ebook request // Check for existing ebook request
let ebookRequest = await prisma.request.findFirst({ // If we were given an ebook request ID directly, use that; otherwise search by parent
where: { let ebookRequest = foundRequest.type === 'ebook'
parentRequestId, ? foundRequest
type: 'ebook', : await prisma.request.findFirst({
deletedAt: null, where: {
}, parentRequestId: parentRequest.id,
}); type: 'ebook',
deletedAt: null,
},
});
if (ebookRequest && !['failed', 'awaiting_search', 'pending'].includes(ebookRequest.status)) { if (ebookRequest && !['failed', 'awaiting_search', 'pending'].includes(ebookRequest.status)) {
return NextResponse.json({ return NextResponse.json({
@@ -109,9 +127,10 @@ export async function POST(
userId: parentRequest.userId, userId: parentRequest.userId,
audiobookId: parentRequest.audiobookId, audiobookId: parentRequest.audiobookId,
type: 'ebook', type: 'ebook',
parentRequestId, parentRequestId: parentRequest.id,
status: 'searching', status: 'searching',
progress: 0, progress: 0,
customSearchTerms: parentRequest.customSearchTerms,
}, },
}); });
logger.info(`Created new ebook request ${ebookRequest.id}`); logger.info(`Created new ebook request ${ebookRequest.id}`);
+13 -4
View File
@@ -36,16 +36,25 @@ export async function processSearchEbook(payload: SearchEbookPayload): Promise<a
logger.info(`Processing ebook request ${requestId} for "${audiobook.title}"`); logger.info(`Processing ebook request ${requestId} for "${audiobook.title}"`);
try { try {
// Update request status to searching // Update request status to searching and fetch custom search terms
await prisma.request.update({ const requestRecord = await prisma.request.update({
where: { id: requestId }, where: { id: requestId },
data: { data: {
status: 'searching', status: 'searching',
searchAttempts: { increment: 1 }, searchAttempts: { increment: 1 },
updatedAt: new Date(), updatedAt: new Date(),
}, },
select: { customSearchTerms: true },
}); });
// Use custom search terms if set, otherwise use audiobook title
const effectiveSearchTitle = requestRecord?.customSearchTerms || audiobook.title;
const searchAudiobook = { ...audiobook, title: effectiveSearchTitle };
if (requestRecord?.customSearchTerms) {
logger.info(`Using custom search terms: "${effectiveSearchTitle}" (original: "${audiobook.title}")`);
}
// Get ebook configuration // Get ebook configuration
const configService = getConfigService(); const configService = getConfigService();
const preferredFormat = payloadFormat || await configService.get('ebook_sidecar_preferred_format') || 'epub'; const preferredFormat = payloadFormat || await configService.get('ebook_sidecar_preferred_format') || 'epub';
@@ -62,7 +71,7 @@ export async function processSearchEbook(payload: SearchEbookPayload): Promise<a
// ========== STEP 1: Try Anna's Archive (if enabled) ========== // ========== STEP 1: Try Anna's Archive (if enabled) ==========
if (annasArchiveEnabled) { if (annasArchiveEnabled) {
logger.info(`Searching Anna's Archive...`); logger.info(`Searching Anna's Archive...`);
annasArchiveResult = await searchAnnasArchive(audiobook, preferredFormat, logger); annasArchiveResult = await searchAnnasArchive(searchAudiobook, preferredFormat, logger);
if (annasArchiveResult) { if (annasArchiveResult) {
logger.info(`Found ebook via Anna's Archive (score: ${annasArchiveResult.score})`); logger.info(`Found ebook via Anna's Archive (score: ${annasArchiveResult.score})`);
@@ -74,7 +83,7 @@ export async function processSearchEbook(payload: SearchEbookPayload): Promise<a
// ========== STEP 2: Try Indexer Search (if enabled and no Anna's Archive result) ========== // ========== STEP 2: Try Indexer Search (if enabled and no Anna's Archive result) ==========
if (!annasArchiveResult && indexerSearchEnabled) { if (!annasArchiveResult && indexerSearchEnabled) {
logger.info(`Searching indexers...`); logger.info(`Searching indexers...`);
indexerResult = await searchIndexers(requestId, audiobook, preferredFormat, logger); indexerResult = await searchIndexers(requestId, searchAudiobook, preferredFormat, logger);
if (indexerResult) { if (indexerResult) {
logger.info(`Found ebook via indexer search (score: ${indexerResult.finalScore.toFixed(1)})`); logger.info(`Found ebook via indexer search (score: ${indexerResult.finalScore.toFixed(1)})`);