From 1374e66f13ee4b806e72264fa91303f2199e083b Mon Sep 17 00:00:00 2001 From: kikootwo Date: Wed, 24 Dec 2025 02:52:29 -0500 Subject: [PATCH] Improve torrent search and ranking algorithm Enhanced the ranking algorithm to better distinguish complete title matches from partial matches, reducing series confusion. Updated the torrent search modal to allow users to customize the search title for new requests, improving search flexibility. Also refined request lookup to ignore deleted requests. --- documentation/phase3/ranking-algorithm.md | 7 ++- .../audiobooks/request-with-torrent/route.ts | 3 +- .../InteractiveTorrentSearchModal.tsx | 60 +++++++++++++++++-- src/lib/utils/ranking-algorithm.ts | 22 ++++++- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/documentation/phase3/ranking-algorithm.md b/documentation/phase3/ranking-algorithm.md index 5c86d34..0fd997f 100644 --- a/documentation/phase3/ranking-algorithm.md +++ b/documentation/phase3/ranking-algorithm.md @@ -8,12 +8,15 @@ Evaluates and scores torrents to automatically select best audiobook download. **1. Title/Author Match (50 pts max) - MOST IMPORTANT** - Title matching: 0-35 pts - - Exact substring match → 35 pts + - Complete title match (followed by metadata: " by", " [", " -") → 35 pts + - Title is substring but continues with more words → fuzzy similarity (partial credit) + - Prevents series confusion: "The Housemaid" vs "The Housemaid's Secret" - No exact match → fuzzy similarity (partial credit) - Author presence: 0-15 pts + - Exact substring match → proportional credit + - No exact match → fuzzy similarity (partial credit) - Splits authors on delimiters (comma, &, "and", " - ") - Filters out roles ("translator", "narrator") - - Proportional credit for partial matches - Order-independent, no structure assumptions - Ensures correct book is selected over wrong book with better format diff --git a/src/app/api/audiobooks/request-with-torrent/route.ts b/src/app/api/audiobooks/request-with-torrent/route.ts index ef0b73f..b83f641 100644 --- a/src/app/api/audiobooks/request-with-torrent/route.ts +++ b/src/app/api/audiobooks/request-with-torrent/route.ts @@ -96,11 +96,12 @@ export async function POST(request: NextRequest) { }); } - // Check if user already has an active request for this audiobook + // Check if user already has an active (non-deleted) request for this audiobook const existingRequest = await prisma.request.findFirst({ where: { userId: req.user.id, audiobookId: audiobookRecord.id, + deletedAt: null, // Only check active requests }, }); diff --git a/src/components/requests/InteractiveTorrentSearchModal.tsx b/src/components/requests/InteractiveTorrentSearchModal.tsx index 0d3b1ae..4ed2848 100644 --- a/src/components/requests/InteractiveTorrentSearchModal.tsx +++ b/src/components/requests/InteractiveTorrentSearchModal.tsx @@ -43,6 +43,7 @@ export function InteractiveTorrentSearchModal({ const [results, setResults] = useState<(TorrentResult & { rank: number; qualityScore?: number })[]>([]); const [confirmTorrent, setConfirmTorrent] = useState(null); + const [searchTitle, setSearchTitle] = useState(audiobook.title); // Determine which mode we're in const hasRequestId = !!requestId; @@ -52,6 +53,12 @@ export function InteractiveTorrentSearchModal({ ? (searchByRequestError || selectTorrentError) : (searchByAudiobookError || requestWithTorrentError); + // Reset search title when modal opens/closes or audiobook changes + React.useEffect(() => { + setSearchTitle(audiobook.title); + setResults([]); + }, [isOpen, audiobook.title]); + // Perform search when modal opens React.useEffect(() => { if (isOpen && results.length === 0) { @@ -60,14 +67,17 @@ export function InteractiveTorrentSearchModal({ }, [isOpen]); const performSearch = async () => { + // Clear existing results while searching + setResults([]); + try { let data; if (hasRequestId) { - // Existing flow: search by requestId + // Existing flow: search by requestId (cannot customize search term) data = await searchByRequestId(requestId); } else { - // New flow: search by audiobook title/author - data = await searchByAudiobook(audiobook.title, audiobook.author); + // New flow: search by custom title + original author + data = await searchByAudiobook(searchTitle, audiobook.author); } setResults(data || []); } catch (err) { @@ -76,6 +86,12 @@ export function InteractiveTorrentSearchModal({ } }; + const handleSearchKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + performSearch(); + } + }; + const handleDownloadClick = (torrent: TorrentResult) => { setConfirmTorrent(torrent); }; @@ -124,10 +140,42 @@ export function InteractiveTorrentSearchModal({ <>
- {/* Audiobook info */} + {/* Search customization */}
-

{audiobook.title}

-

By {audiobook.author}

+ {hasRequestId ? ( + // Existing request: show static title (cannot customize) + <> +

{audiobook.title}

+

By {audiobook.author}

+ + ) : ( + // New search: allow title customization + <> + +
+ setSearchTitle(e.target.value)} + onKeyPress={handleSearchKeyPress} + placeholder="Enter book title to search..." + disabled={isSearching} + className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 disabled:opacity-50" + /> + +
+

By {audiobook.author}

+ + )}
{/* Error message */} diff --git a/src/lib/utils/ranking-algorithm.ts b/src/lib/utils/ranking-algorithm.ts index a8b98ab..185de37 100644 --- a/src/lib/utils/ranking-algorithm.ts +++ b/src/lib/utils/ranking-algorithm.ts @@ -201,10 +201,26 @@ export class RankingAlgorithm { // Title matching (0-35 points) let titleScore = 0; if (torrentTitle.includes(requestTitle)) { - // Exact substring match → full points - titleScore = 35; + // Found the title, but is it the complete title or part of a longer one? + const titleIndex = torrentTitle.indexOf(requestTitle); + const afterTitle = torrentTitle.substring(titleIndex + requestTitle.length); + + // Title is complete if followed by clear metadata markers + // (not followed by more title words like "'s Secret" or " Is Watching") + const metadataMarkers = [' by ', ' - ', ' [', ' (', ' {', ' :', ',']; + const isCompleteTitle = afterTitle === '' || + metadataMarkers.some(marker => afterTitle.startsWith(marker)); + + if (isCompleteTitle) { + // Complete title match → full points + titleScore = 35; + } else { + // Title continues with more words (e.g., "The Housemaid" + "'s Secret") + // This is likely a different book in a series → use fuzzy similarity + titleScore = compareTwoStrings(requestTitle, torrentTitle) * 35; + } } else { - // No exact match → use fuzzy similarity for partial credit + // No substring match at all → use fuzzy similarity titleScore = compareTwoStrings(requestTitle, torrentTitle) * 35; }