mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
f043688a71
Adds soft-delete support for local users, including backend, API, and UI changes to allow admins to delete local users while preserving their requests. Updates user queries to exclude deleted users and allows username reuse for deleted accounts. Refines search and ranking logic for torrents: uses title-only queries for broader results, increases max results to 100, applies a minimum score threshold (30/100), and logs detailed ranking breakdowns. Updates the ranking algorithm to prioritize title/author match, adjusts scoring weights, and improves BookDate compatibility with Audiobookshelf by disabling rating-based features when unsupported. Enhances file copy operations for large files, improves metadata tagging, and updates documentation to reflect new search and ranking strategies.
149 lines
5.7 KiB
TypeScript
149 lines
5.7 KiB
TypeScript
/**
|
|
* Component: Audiobook Torrent Search API
|
|
* Documentation: documentation/phase3/prowlarr.md
|
|
*
|
|
* Search for torrents without creating a request first
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
|
|
import { getProwlarrService } from '@/lib/integrations/prowlarr.service';
|
|
import { rankTorrents } from '@/lib/utils/ranking-algorithm';
|
|
import { z } from 'zod';
|
|
|
|
const SearchSchema = z.object({
|
|
title: z.string(),
|
|
author: z.string(),
|
|
});
|
|
|
|
/**
|
|
* POST /api/audiobooks/search-torrents
|
|
* Search for torrents for an audiobook (no request required)
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
|
try {
|
|
if (!req.user) {
|
|
return NextResponse.json(
|
|
{ error: 'Unauthorized', message: 'User not authenticated' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const body = await req.json();
|
|
const { title, author } = SearchSchema.parse(body);
|
|
|
|
// Get enabled indexers from configuration
|
|
const { getConfigService } = await import('@/lib/services/config.service');
|
|
const configService = getConfigService();
|
|
const indexersConfigStr = await configService.get('prowlarr_indexers');
|
|
|
|
if (!indexersConfigStr) {
|
|
return NextResponse.json(
|
|
{ error: 'ConfigError', message: 'No indexers configured. Please configure indexers in settings.' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const indexersConfig = JSON.parse(indexersConfigStr);
|
|
const enabledIndexerIds = indexersConfig.map((indexer: any) => indexer.id);
|
|
|
|
if (enabledIndexerIds.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'ConfigError', message: 'No indexers enabled. Please enable at least one indexer in settings.' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Search Prowlarr for torrents - ONLY enabled indexers
|
|
const prowlarr = await getProwlarrService();
|
|
const searchQuery = title; // Title only - cast wide net
|
|
|
|
console.log(`[AudiobookSearch] Searching ${enabledIndexerIds.length} enabled indexers for: ${searchQuery}`);
|
|
|
|
const results = await prowlarr.search(searchQuery, {
|
|
indexerIds: enabledIndexerIds,
|
|
maxResults: 100, // Increased limit for broader search
|
|
});
|
|
|
|
console.log(`[AudiobookSearch] Found ${results.length} raw results for "${title}" by ${author}`);
|
|
|
|
if (results.length === 0) {
|
|
return NextResponse.json({
|
|
success: true,
|
|
results: [],
|
|
message: 'No torrents found',
|
|
});
|
|
}
|
|
|
|
// Rank torrents using the ranking algorithm
|
|
const rankedResults = rankTorrents(results, { title, author });
|
|
|
|
// Filter out results below minimum score threshold (30/100)
|
|
const filteredResults = rankedResults.filter(result => result.score >= 30);
|
|
|
|
console.log(`[AudiobookSearch] Ranked ${rankedResults.length} results, ${filteredResults.length} above threshold (30/100)`);
|
|
|
|
// Log top 3 results with detailed score breakdown for debugging
|
|
const top3 = filteredResults.slice(0, 3);
|
|
if (top3.length > 0) {
|
|
console.log(`[AudiobookSearch] ==================== RANKING DEBUG ====================`);
|
|
console.log(`[AudiobookSearch] Requested Title: "${title}"`);
|
|
console.log(`[AudiobookSearch] Requested Author: "${author}"`);
|
|
console.log(`[AudiobookSearch] Top ${top3.length} results (out of ${filteredResults.length} above threshold):`);
|
|
console.log(`[AudiobookSearch] --------------------------------------------------------`);
|
|
top3.forEach((result, index) => {
|
|
console.log(`[AudiobookSearch] ${index + 1}. "${result.title}"`);
|
|
console.log(`[AudiobookSearch] Indexer: ${result.indexer}`);
|
|
console.log(`[AudiobookSearch] Total Score: ${result.score.toFixed(1)}/100`);
|
|
console.log(`[AudiobookSearch] - Title/Author Match: ${result.breakdown.matchScore.toFixed(1)}/50`);
|
|
console.log(`[AudiobookSearch] - Format Quality: ${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`);
|
|
console.log(`[AudiobookSearch] - Seeder Count: ${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders} seeders)`);
|
|
console.log(`[AudiobookSearch] - Size Score: ${result.breakdown.sizeScore.toFixed(1)}/10 (${(result.size / (1024 ** 3)).toFixed(2)} GB)`);
|
|
if (result.breakdown.notes.length > 0) {
|
|
console.log(`[AudiobookSearch] Notes: ${result.breakdown.notes.join(', ')}`);
|
|
}
|
|
if (index < top3.length - 1) {
|
|
console.log(`[AudiobookSearch] --------------------------------------------------------`);
|
|
}
|
|
});
|
|
console.log(`[AudiobookSearch] ========================================================`);
|
|
}
|
|
|
|
// Add rank position to each result
|
|
const resultsWithRank = filteredResults.map((result, index) => ({
|
|
...result,
|
|
rank: index + 1,
|
|
}));
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
results: resultsWithRank,
|
|
message: filteredResults.length > 0
|
|
? `Found ${filteredResults.length} quality matches`
|
|
: 'No quality matches found',
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to search for torrents:', error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return NextResponse.json(
|
|
{
|
|
error: 'ValidationError',
|
|
details: error.errors,
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: 'SearchError',
|
|
message: error instanceof Error ? error.message : 'Failed to search for torrents',
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
});
|
|
}
|