/** * Component: Interactive Torrent Search Modal * Documentation: documentation/phase3/prowlarr.md * * Supports two search modes: * - audiobook: Search for audiobook torrents/NZBs (default) * - ebook: Search for ebooks from Anna's Archive + indexers */ 'use client'; import React, { useState } from 'react'; import { Modal } from '@/components/ui/Modal'; import { Button } from '@/components/ui/Button'; import { ConfirmModal } from '@/components/ui/ConfirmModal'; import { TorrentResult, RankedTorrent } from '@/lib/utils/ranking-algorithm'; import { useInteractiveSearch, useSelectTorrent, useSearchTorrents, useRequestWithTorrent, useInteractiveSearchEbook, useSelectEbook, useInteractiveSearchEbookByAsin, useSelectEbookByAsin, } from '@/lib/hooks/useRequests'; import { Audiobook } from '@/lib/hooks/useAudiobooks'; interface InteractiveTorrentSearchModalProps { isOpen: boolean; onClose: () => void; requestId?: string; // Optional - only provided when called from existing request asin?: string; // Optional - ASIN for ebook mode when no request exists audiobook: { title: string; author: string; }; fullAudiobook?: Audiobook; // Optional - only provided when called from details modal onSuccess?: () => void; searchMode?: 'audiobook' | 'ebook'; // Search mode - defaults to audiobook } export function InteractiveTorrentSearchModal({ isOpen, onClose, requestId, asin, audiobook, fullAudiobook, onSuccess, searchMode = 'audiobook', }: InteractiveTorrentSearchModalProps) { // Hooks for existing audiobook request flow const { searchTorrents: searchByRequestId, isLoading: isSearchingByRequest, error: searchByRequestError } = useInteractiveSearch(); const { selectTorrent, isLoading: isSelectingTorrent, error: selectTorrentError } = useSelectTorrent(); // Hooks for new audiobook flow const { searchTorrents: searchByAudiobook, isLoading: isSearchingByAudiobook, error: searchByAudiobookError } = useSearchTorrents(); const { requestWithTorrent, isLoading: isRequestingWithTorrent, error: requestWithTorrentError } = useRequestWithTorrent(); // Hooks for ebook flow (request ID-based - admin) const { searchEbooks, isLoading: isSearchingEbooks, error: searchEbooksError } = useInteractiveSearchEbook(); const { selectEbook, isLoading: isSelectingEbook, error: selectEbookError } = useSelectEbook(); // Hooks for ebook flow (ASIN-based - user) const { searchEbooks: searchEbooksByAsin, isLoading: isSearchingEbooksByAsin, error: searchEbooksByAsinError } = useInteractiveSearchEbookByAsin(); const { selectEbook: selectEbookByAsin, isLoading: isSelectingEbookByAsin, error: selectEbookByAsinError } = useSelectEbookByAsin(); const [results, setResults] = useState<(RankedTorrent & { qualityScore?: number; source?: string })[]>([]); const [confirmTorrent, setConfirmTorrent] = useState(null); const [searchTitle, setSearchTitle] = useState(audiobook.title); // Determine which mode we're in const isEbookMode = searchMode === 'ebook'; const hasRequestId = !!requestId; const hasAsin = !!asin; const useAsinMode = isEbookMode && hasAsin && !hasRequestId; // Loading/error state based on mode const isSearching = isEbookMode ? (useAsinMode ? isSearchingEbooksByAsin : isSearchingEbooks) : (hasRequestId ? isSearchingByRequest : isSearchingByAudiobook); const isDownloading = isEbookMode ? (useAsinMode ? isSelectingEbookByAsin : isSelectingEbook) : (hasRequestId ? isSelectingTorrent : isRequestingWithTorrent); const error = isEbookMode ? (useAsinMode ? (searchEbooksByAsinError || selectEbookByAsinError) : (searchEbooksError || selectEbookError)) : (hasRequestId ? (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) { performSearch(); } }, [isOpen]); const performSearch = async () => { // Clear existing results while searching setResults([]); try { let data; if (isEbookMode) { // Ebook mode: search Anna's Archive + indexers const customTitle = searchTitle !== audiobook.title ? searchTitle : undefined; if (useAsinMode && asin) { // ASIN-based ebook search (user flow from details modal) data = await searchEbooksByAsin(asin, customTitle); } else if (requestId) { // Request ID-based ebook search (admin flow) data = await searchEbooks(requestId, customTitle); } else { console.error('Ebook search requires either requestId or asin'); return; } } else if (hasRequestId) { // Existing audiobook flow: search by requestId with optional custom title const customTitle = searchTitle !== audiobook.title ? searchTitle : undefined; data = await searchByRequestId(requestId, customTitle); } else { // New audiobook flow: search by custom title + original author + optional ASIN for size scoring const audiobookAsin = fullAudiobook?.asin; data = await searchByAudiobook(searchTitle, audiobook.author, audiobookAsin); } setResults(data || []); } catch (err) { // Error already handled by hook console.error('Search failed:', err); } }; const handleSearchKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { performSearch(); } }; const handleDownloadClick = (torrent: TorrentResult) => { setConfirmTorrent(torrent); }; const handleConfirmDownload = async () => { if (!confirmTorrent) return; try { if (isEbookMode) { // Ebook flow if (useAsinMode && asin) { // ASIN-based ebook selection (user flow from details modal) await selectEbookByAsin(asin, confirmTorrent); } else if (requestId) { // Request ID-based ebook selection (admin flow) await selectEbook(requestId, confirmTorrent); } else { throw new Error('Request ID or ASIN required for ebook selection'); } } else if (hasRequestId) { // Existing audiobook flow: select torrent for existing request await selectTorrent(requestId, confirmTorrent); } else { // New audiobook flow: create request with torrent if (!fullAudiobook) { throw new Error('Audiobook data required to create request'); } await requestWithTorrent(fullAudiobook, confirmTorrent); } // Notify parent of successful selection onSuccess?.(); // Close modals on success setConfirmTorrent(null); onClose(); // Request list will auto-refresh via SWR } catch (err) { // Error already handled by hook console.error('Failed to download:', err); setConfirmTorrent(null); } }; const formatSize = (bytes: number) => { const gb = bytes / (1024 ** 3); const mb = bytes / (1024 ** 2); return gb >= 1 ? `${gb.toFixed(1)} GB` : `${mb.toFixed(0)} MB`; }; const getQualityBadgeColor = (score: number) => { if (score >= 90) return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; if (score >= 70) return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; if (score >= 50) return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'; }; // UI text based on mode const modalTitle = isEbookMode ? 'Select Ebook Source' : 'Select Torrent'; const searchLabel = isEbookMode ? 'Search Title' : 'Search Title'; const searchPlaceholder = isEbookMode ? 'Enter book title to search...' : 'Enter book title to search...'; const loadingText = isEbookMode ? 'Searching for ebooks...' : 'Searching for torrents...'; const noResultsText = isEbookMode ? 'No ebooks found' : 'No torrents/nzbs found'; const resultCountText = (count: number) => isEbookMode ? `Found ${count} ebook${count !== 1 ? 's' : ''}` : `Found ${count} torrent${count !== 1 ? 's' : ''}`; const confirmTitle = isEbookMode ? 'Download Ebook' : 'Download Torrent'; return ( <>
{/* Search customization - editable for ALL modes */}
setSearchTitle(e.target.value)} onKeyPress={handleSearchKeyPress} placeholder={searchPlaceholder} 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 */} {error && (

{error}

)} {/* Loading state */} {isSearching && (
{loadingText}
)} {/* No results */} {!isSearching && results.length === 0 && (

{noResultsText}

)} {/* Results table */} {!isSearching && results.length > 0 && (
{results.map((result) => ( ))}
# Title Size Score Bonus Seeds {isEbookMode ? 'Source' : 'Indexer'} Action
{result.rank}
{/* Anna's Archive badge for ebook mode */} {isEbookMode && result.source === 'annas_archive' && ( Anna's Archive )} {result.format && ( {result.format} )} {result.size > 0 ? formatSize(result.size) : 'Unknown'} {/* Hide seeds badge for Anna's Archive results */} {!(isEbookMode && result.source === 'annas_archive') && ( {result.seeders} seeds )}
{result.size > 0 ? formatSize(result.size) : '—'} {Math.round(result.score)} {result.bonusPoints > 0 ? `+${Math.round(result.bonusPoints)}` : '—'} {isEbookMode && result.source === 'annas_archive' ? ( N/A ) : ( {result.seeders} )} {isEbookMode && result.source === 'annas_archive' ? ( Anna's Archive ) : ( result.indexer )}
)} {/* Footer with result count */} {!isSearching && results.length > 0 && (

{resultCountText(results.length)}

)}
{/* Confirmation Modal */} setConfirmTorrent(null)} onConfirm={handleConfirmDownload} title={confirmTitle} message={`Download "${confirmTorrent?.title}"?`} confirmText="Download" isLoading={isDownloading} variant="primary" /> ); }