From 23881eb6708e494f068d593a48dd5303fca8d4cf Mon Sep 17 00:00:00 2001 From: kikootwo Date: Tue, 6 Jan 2026 20:10:33 -0500 Subject: [PATCH] Add indexer flag bonuses and SSL verify toggle Implements configurable indexer flag bonuses/penalties for torrent ranking, including UI for admin settings and support in ranking-algorithm. Adds an option to disable SSL certificate verification for qBittorrent connections (for self-signed certs), with UI in both setup and admin settings, and persists the setting. Updates documentation, API routes, and ranking logic to support these features. Also includes minor UI improvements and bug fixes. --- documentation/phase3/qbittorrent.md | 14 +- documentation/phase3/ranking-algorithm.md | 101 +++++++++-- next.config.ts | 4 +- src/app/admin/settings/page.tsx | 94 ++++++++++- .../admin/settings/download-client/route.ts | 11 ++ .../admin/settings/prowlarr/indexers/route.ts | 25 ++- src/app/api/admin/settings/route.ts | 1 + .../settings/test-download-client/route.ts | 4 +- .../api/audiobooks/search-torrents/route.ts | 44 ++++- .../requests/[id]/interactive-search/route.ts | 44 ++++- src/app/api/setup/complete/route.ts | 9 + .../api/setup/test-download-client/route.ts | 5 +- src/app/login/page.tsx | 24 --- src/app/page.tsx | 25 +-- src/app/setup/page.tsx | 4 + src/app/setup/steps/DownloadClientStep.tsx | 30 ++++ src/components/admin/FlagConfigRow.tsx | 143 ++++++++++++++++ src/components/audiobooks/AudiobookCard.tsx | 10 +- .../audiobooks/AudiobookDetailsModal.tsx | 14 +- .../InteractiveTorrentSearchModal.tsx | 28 ++-- src/components/ui/StickyPagination.tsx | 28 +++- src/lib/integrations/audible.service.ts | 8 + src/lib/integrations/prowlarr.service.ts | 66 ++++++++ src/lib/integrations/qbittorrent.service.ts | 119 ++++++++++++- .../processors/search-indexers.processor.ts | 50 +++++- src/lib/utils/ranking-algorithm.ts | 157 +++++++++++++++--- 26 files changed, 921 insertions(+), 141 deletions(-) create mode 100644 src/components/admin/FlagConfigRow.tsx diff --git a/documentation/phase3/qbittorrent.md b/documentation/phase3/qbittorrent.md index dc24cd0..8ca0bc8 100644 --- a/documentation/phase3/qbittorrent.md +++ b/documentation/phase3/qbittorrent.md @@ -43,11 +43,17 @@ Free, open-source BitTorrent client with comprehensive Web API. ## Config **Required (database only, no env fallbacks):** -- `download_client_url` - qBittorrent Web UI URL +- `download_client_url` - qBittorrent Web UI URL (supports HTTP and HTTPS) - `download_client_username` - qBittorrent username - `download_client_password` - qBittorrent password - `download_dir` - Download save path (passed to qBittorrent for all torrents) +**Optional (SSL/TLS):** +- `download_client_disable_ssl_verify` - Disable SSL certificate verification for HTTPS (boolean as string "true"/"false", default: "false") + - Use when connecting to qBittorrent with self-signed certificates + - ⚠️ Security warning: Only use on trusted private networks + - Enhanced error messages guide users when SSL issues detected + **Optional (Remote Path Mapping):** - `download_client_remote_path_mapping_enabled` - Enable path mapping (boolean as string "true"/"false") - `download_client_remote_path` - Remote path prefix from qBittorrent @@ -164,6 +170,12 @@ type TorrentState = 'downloading' | 'uploading' | 'stalledDL' | - PathMapper utility for prefix replacement transformation - Local path validation during test connection - Applied in download completion and import retry processors +**11. HTTPS SSL certificate errors** - Users with seedboxes using self-signed certificates or Let's Encrypt couldn't connect. Fixed by: + - Optional SSL verification disable toggle in setup wizard and admin settings + - Custom HTTPS agent with `rejectUnauthorized: false` when enabled + - Enhanced error messages identifying SSL/TLS certificate issues with actionable guidance + - Secure by default (SSL verification enabled), with clear security warnings when disabled + - URL format: `https://qbt.domain.com:443/qbittorrent` fully supported ## Tech Stack diff --git a/documentation/phase3/ranking-algorithm.md b/documentation/phase3/ranking-algorithm.md index 5e3b4ec..3de984b 100644 --- a/documentation/phase3/ranking-algorithm.md +++ b/documentation/phase3/ranking-algorithm.md @@ -12,19 +12,27 @@ Evaluates and scores torrents to automatically select best audiobook download. **Stage 1: Word Coverage Filter (MANDATORY)** - Extracts significant words from request (filters stop words: "the", "a", "an", "of", "on", "in", "at", "by", "for") -- Calculates coverage: % of request words found in torrent title -- **Hard requirement: 80%+ coverage or automatic 0 score** +- **Parenthetical/bracketed content is optional**: Content in () [] {} treated as subtitle (may be omitted from torrents) + - "We Are Legion (We Are Bob)" → Required: ["we", "are", "legion"], Optional: ["bob"] + - "Title [Series Name]" → Required: ["title"], Optional: ["series", "name"] +- Calculates coverage: % of **required** words found in torrent title +- **Hard requirement: 80%+ coverage of required words or automatic 0 score** - Example: "The Wild Robot on the Island" → ["wild", "robot", "island"] - "The Wild Robot" → ["wild", "robot"] → 2/3 = 67% → **REJECTED** - "The Wild Robot on the Island" → 3/3 = 100% → **PASSES** -- Prevents wrong series books from matching +- Example: "We Are Legion (We Are Bob)" → Required: ["we", "are", "legion"] + - "Dennis E. Taylor - Bobiverse - 01 - We Are Legion" → 3/3 = 100% → **PASSES** +- Prevents wrong series books from matching while handling common subtitle patterns **Stage 2: Title Matching (0-35 pts)** - Only scored if Stage 1 passes -- 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) +- Complete title match requirements (both must be true): + - No significant words BEFORE matched title (prevents "This Inevitable Ruin Dungeon Crawler Carl, Book 7") + - Followed by metadata markers: " by", " [", " -", " (", " {", " :", "," +- Complete match → 35 pts +- Title has prefix/suffix words OR continues with more words → fuzzy similarity (partial credit) +- Prevents series confusion: "The Housemaid" vs "The Housemaid's Secret", "Dungeon Crawler Carl" vs "Book 7" +- No substring match → fuzzy similarity (partial credit) **Stage 3: Author Matching (0-15 pts)** - Exact substring match → proportional credit @@ -52,25 +60,98 @@ Evaluates and scores torrents to automatically select best audiobook download. - Deviation → penalty - Unknown duration: 5 pts (neutral) +## Bonus Points System + +**Extensible multiplicative bonus system** for external quality factors: + +**Indexer Priority Bonus (configurable 1-25, default: 10)** +- Formula: `bonusPoints = baseScore × (priority / 25)` +- Priority 10/25 (40%) → 95 base score → +38 bonus = 133 final +- Priority 20/25 (80%) → 95 base score → +76 bonus = 171 final +- Priority 25/25 (100%) → 95 base score → +95 bonus = 190 final +- Ensures high-quality torrent from low-priority indexer beats low-quality from high-priority +- Bonus scales with quality (better torrents get more benefit from priority) + +**Indexer Flag Bonus (configurable -100% to +100%, default: 0%)** +- Formula: `bonusPoints = baseScore × (modifier / 100)` +- Positive modifiers reward desired flags (e.g., "Freeleech" at +50%) + - +50% modifier → 85 base score → +42.5 bonus = 127.5 final +- Negative modifiers penalize undesired flags (e.g., "Unwanted" at -60%) + - -60% modifier → 85 base score → -51 penalty = 34 final +- Dual threshold filtering: + - Base score must be ≥ 50 (quality minimum) + - Final score must be ≥ 50 (not disqualified by negative bonuses) + - Negative bonuses can disqualify otherwise good torrents +- Flag extraction from Prowlarr API: + - `downloadVolumeFactor: 0` → "Freeleech" + - `downloadVolumeFactor: <1` → "Partial Freeleech" + - `uploadVolumeFactor: >1` → "Double Upload" +- Case-insensitive, whitespace-trimmed matching +- Universal across all indexers (not indexer-specific) +- Multiple flag bonuses stack (additive) + +**Future Modifiers (planned):** +- User preferences +- Custom rules + +**Final Score Calculation:** +1. Calculate base score (0-100) using standard criteria +2. Calculate bonus modifiers (indexer priority, flag bonuses, etc.) +3. Sum bonus points +4. Final score = base score + bonus points +5. Apply dual threshold filter: + - Base score ≥ 50 (quality minimum) + - Final score ≥ 50 (not disqualified by negative bonuses) +6. Sort by final score (descending), then publish date (descending) + +## Tiebreaker Sorting + +When multiple torrents have identical final scores: +- **Secondary sort:** Publish date descending (newest first) +- Ensures latest uploads are preferred when quality is equal +- Example: 3 torrents with 171 final score → newest upload ranks #1 + ## Interface ```typescript +interface IndexerFlagConfig { + name: string; // Flag name (e.g., "Freeleech") + modifier: number; // -100 to 100 (percentage) +} + +interface BonusModifier { + type: 'indexer_priority' | 'indexer_flag' | 'custom'; + value: number; // Multiplier (e.g., 0.4 for 40%) + points: number; // Calculated bonus points + reason: string; // Human-readable explanation +} + +interface TorrentResult { + // ... existing fields + flags?: string[]; // Extracted flags from Prowlarr API +} + interface RankedTorrent extends TorrentResult { - score: number; + score: number; // Base score (0-100) + bonusModifiers: BonusModifier[]; + bonusPoints: number; // Sum of all bonus points + finalScore: number; // score + bonusPoints rank: number; breakdown: { formatScore: number; seederScore: number; sizeScore: number; matchScore: number; - totalScore: number; + totalScore: number; // Same as score notes: string[]; }; } function rankTorrents( torrents: TorrentResult[], - audiobook: AudiobookRequest + audiobook: AudiobookRequest, + indexerPriorities?: Map, // indexerId -> priority (1-25) + flagConfigs?: IndexerFlagConfig[] // Flag bonus configurations ): RankedTorrent[]; ``` diff --git a/next.config.ts b/next.config.ts index 4b945b1..58fa81d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -26,8 +26,10 @@ const nextConfig: NextConfig = { return config; }, - // Image optimization + // Image optimization - DISABLED because we handle our own thumbnail caching + // in /app/cache/thumbnails/ via the Audible refresh job images: { + unoptimized: true, // Disable Next.js image optimization remotePatterns: [ { protocol: 'https', diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx index 09411e7..6cddabf 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/admin/settings/page.tsx @@ -10,6 +10,8 @@ import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import Link from 'next/link'; import { fetchWithAuth } from '@/lib/utils/api'; +import { IndexerFlagConfig } from '@/lib/utils/ranking-algorithm'; +import { FlagConfigRow } from '@/components/admin/FlagConfigRow'; interface PlexLibrary { id: string; @@ -70,6 +72,7 @@ interface Settings { url: string; username: string; password: string; + disableSSLVerify: boolean; remotePathMappingEnabled: boolean; remotePath: string; localPath: string; @@ -102,6 +105,7 @@ export default function AdminSettings() { const [plexLibraries, setPlexLibraries] = useState([]); const [absLibraries, setAbsLibraries] = useState([]); const [indexers, setIndexers] = useState([]); + const [flagConfigs, setFlagConfigs] = useState([]); const [pendingUsers, setPendingUsers] = useState([]); const [isLocalAdmin, setIsLocalAdmin] = useState(false); const [loading, setLoading] = useState(true); @@ -294,6 +298,7 @@ export default function AdminSettings() { if (response.ok) { const data = await response.json(); setIndexers(data.indexers || []); + setFlagConfigs(data.flagConfigs || []); } else { console.error('Failed to fetch indexers:', response.status); // Don't show error on initial load, only if user explicitly tries to load @@ -651,6 +656,7 @@ export default function AdminSettings() { url: settings.downloadClient.url, username: settings.downloadClient.username, password: settings.downloadClient.password, + disableSSLVerify: settings.downloadClient.disableSSLVerify, remotePathMappingEnabled: settings.downloadClient.remotePathMappingEnabled, remotePath: settings.downloadClient.remotePath, localPath: settings.downloadClient.localPath, @@ -846,12 +852,12 @@ export default function AdminSettings() { throw new Error('Failed to save Prowlarr settings'); } - // Save indexer configuration if indexers are loaded + // Save indexer configuration and flag configs if indexers are loaded if (indexers.length > 0) { const indexersResponse = await fetchWithAuth('/api/admin/settings/prowlarr/indexers', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ indexers }), + body: JSON.stringify({ indexers, flagConfigs }), }); if (!indexersResponse.ok) { @@ -1456,6 +1462,54 @@ export default function AdminSettings() { )} + + {/* Flag Configuration Section */} +
+
+

+ Indexer Flag Configuration (Optional) +

+

+ Configure score bonuses or penalties for indexer flags like "Freeleech". + These modifiers apply universally across all indexers and affect final torrent ranking. +

+
+ + {flagConfigs.length > 0 && ( +
+ {flagConfigs.map((config, index) => ( + { + const newConfigs = [...flagConfigs]; + newConfigs[index] = updated; + setFlagConfigs(newConfigs); + }} + onRemove={() => { + setFlagConfigs(flagConfigs.filter((_, i) => i !== index)); + }} + /> + ))} +
+ )} + + + + {flagConfigs.length === 0 && ( +

+ No flag rules configured. Flag bonuses/penalties are optional. +

+ )} +
)} @@ -1551,6 +1605,42 @@ export default function AdminSettings() { /> + {/* SSL Verification Toggle */} + {settings.downloadClient.url.startsWith('https') && ( +
+
+ { + setSettings({ + ...settings, + downloadClient: { + ...settings.downloadClient, + disableSSLVerify: e.target.checked, + }, + }); + setValidated({ ...validated, download: false }); + }} + className="mt-1 h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" + /> +
+ +

+ Enable this if you're using a self-signed certificate or getting SSL errors. + ⚠️ Only use on trusted private networks. +

+
+
+
+ )} + {/* Remote Path Mapping */}
diff --git a/src/app/api/admin/settings/download-client/route.ts b/src/app/api/admin/settings/download-client/route.ts index fcd1bdd..cbb84b3 100644 --- a/src/app/api/admin/settings/download-client/route.ts +++ b/src/app/api/admin/settings/download-client/route.ts @@ -17,6 +17,7 @@ export async function PUT(request: NextRequest) { url, username, password, + disableSSLVerify, remotePathMappingEnabled, remotePath, localPath, @@ -92,6 +93,16 @@ export async function PUT(request: NextRequest) { }); } + // Save SSL verification setting + await prisma.configuration.upsert({ + where: { key: 'download_client_disable_ssl_verify' }, + update: { value: disableSSLVerify ? 'true' : 'false' }, + create: { + key: 'download_client_disable_ssl_verify', + value: disableSSLVerify ? 'true' : 'false', + }, + }); + // Save remote path mapping configuration await prisma.configuration.upsert({ where: { key: 'download_client_remote_path_mapping_enabled' }, diff --git a/src/app/api/admin/settings/prowlarr/indexers/route.ts b/src/app/api/admin/settings/prowlarr/indexers/route.ts index 3e9e236..a085aca 100644 --- a/src/app/api/admin/settings/prowlarr/indexers/route.ts +++ b/src/app/api/admin/settings/prowlarr/indexers/route.ts @@ -34,6 +34,10 @@ export async function GET(request: NextRequest) { const savedConfigStr = await configService.get('prowlarr_indexers'); const savedIndexers: SavedIndexerConfig[] = savedConfigStr ? JSON.parse(savedConfigStr) : []; + // Get saved flag configuration + const flagConfigStr = await configService.get('indexer_flag_config'); + const flagConfigs = flagConfigStr ? JSON.parse(flagConfigStr) : []; + // Merge with defaults (wizard format: array of {id, name, priority, seedingTimeMinutes}) const savedIndexersMap = new Map( savedIndexers.map((idx) => [idx.id, idx]) @@ -58,6 +62,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ success: true, indexers: indexersWithConfig, + flagConfigs, }); } catch (error) { console.error('[Prowlarr] Failed to fetch indexers:', error); @@ -76,13 +81,13 @@ export async function GET(request: NextRequest) { /** * PUT /api/admin/settings/prowlarr/indexers - * Save indexer configuration + * Save indexer configuration and flag configs */ export async function PUT(request: NextRequest) { return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAdmin(req, async () => { try { - const { indexers } = await req.json(); + const { indexers, flagConfigs } = await req.json(); // Filter to only enabled indexers and convert to wizard format const enabledIndexers = indexers @@ -97,14 +102,26 @@ export async function PUT(request: NextRequest) { // Save to configuration (matches wizard format) const configService = getConfigService(); - await configService.setMany([ + const configUpdates = [ { key: 'prowlarr_indexers', value: JSON.stringify(enabledIndexers), category: 'indexer', description: 'Prowlarr indexer settings (enabled, priority, seeding time)', }, - ]); + ]; + + // Save flag configs if provided + if (flagConfigs !== undefined) { + configUpdates.push({ + key: 'indexer_flag_config', + value: JSON.stringify(flagConfigs), + category: 'indexer', + description: 'Indexer flag bonus/penalty configuration', + }); + } + + await configService.setMany(configUpdates); return NextResponse.json({ success: true, diff --git a/src/app/api/admin/settings/route.ts b/src/app/api/admin/settings/route.ts index 9cd3106..3195acb 100644 --- a/src/app/api/admin/settings/route.ts +++ b/src/app/api/admin/settings/route.ts @@ -71,6 +71,7 @@ export async function GET(request: NextRequest) { url: configMap.get('download_client_url') || '', username: configMap.get('download_client_username') || '', password: maskValue('password', configMap.get('download_client_password')), + disableSSLVerify: configMap.get('download_client_disable_ssl_verify') === 'true', seedingTimeMinutes: parseInt(configMap.get('seeding_time_minutes') || '0'), remotePathMappingEnabled: configMap.get('download_client_remote_path_mapping_enabled') === 'true', remotePath: configMap.get('download_client_remote_path') || '', diff --git a/src/app/api/admin/settings/test-download-client/route.ts b/src/app/api/admin/settings/test-download-client/route.ts index b7cee08..cb6be75 100644 --- a/src/app/api/admin/settings/test-download-client/route.ts +++ b/src/app/api/admin/settings/test-download-client/route.ts @@ -17,6 +17,7 @@ export async function POST(request: NextRequest) { url, username, password, + disableSSLVerify, remotePathMappingEnabled, remotePath, localPath, @@ -57,7 +58,8 @@ export async function POST(request: NextRequest) { const version = await QBittorrentService.testConnectionWithCredentials( url, username, - actualPassword + actualPassword, + disableSSLVerify || false ); // If path mapping enabled, validate local path exists diff --git a/src/app/api/audiobooks/search-torrents/route.ts b/src/app/api/audiobooks/search-torrents/route.ts index 6d69cd9..c8d9487 100644 --- a/src/app/api/audiobooks/search-torrents/route.ts +++ b/src/app/api/audiobooks/search-torrents/route.ts @@ -55,6 +55,15 @@ export async function POST(request: NextRequest) { ); } + // Build indexer priorities map (indexerId -> priority 1-25, default 10) + const indexerPriorities = new Map( + indexersConfig.map((indexer: any) => [indexer.id, indexer.priority ?? 10]) + ); + + // Get flag configurations + const flagConfigStr = await configService.get('indexer_flag_config'); + const flagConfigs = flagConfigStr ? JSON.parse(flagConfigStr) : []; + // Search Prowlarr for torrents - ONLY enabled indexers const prowlarr = await getProwlarrService(); const searchQuery = title; // Title only - cast wide net @@ -76,13 +85,24 @@ export async function POST(request: NextRequest) { }); } - // Rank torrents using the ranking algorithm - const rankedResults = rankTorrents(results, { title, author }); + // Rank torrents using the ranking algorithm with indexer priorities and flag configs + const rankedResults = rankTorrents(results, { title, author }, indexerPriorities, flagConfigs); - // Filter out results below minimum score threshold (50/100) - const filteredResults = rankedResults.filter(result => result.score >= 50); + // Dual threshold filtering: + // 1. Base score must be >= 50 (quality minimum) + // 2. Final score must be >= 50 (not disqualified by negative bonuses) + const filteredResults = rankedResults.filter(result => + result.score >= 50 && result.finalScore >= 50 + ); - console.log(`[AudiobookSearch] Ranked ${rankedResults.length} results, ${filteredResults.length} above threshold (50/100)`); + const disqualifiedByNegativeBonus = rankedResults.filter(result => + result.score >= 50 && result.finalScore < 50 + ).length; + + console.log(`[AudiobookSearch] Ranked ${rankedResults.length} results, ${filteredResults.length} above threshold (50/100 base + final)`); + if (disqualifiedByNegativeBonus > 0) { + console.log(`[AudiobookSearch] ${disqualifiedByNegativeBonus} torrents disqualified by negative flag bonuses`); + } // Log top 3 results with detailed score breakdown for debugging const top3 = filteredResults.slice(0, 3); @@ -94,12 +114,22 @@ export async function POST(request: NextRequest) { 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] Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`); + console.log(`[AudiobookSearch] `); + console.log(`[AudiobookSearch] Base 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)`); + console.log(`[AudiobookSearch] `); + console.log(`[AudiobookSearch] Bonus Points: +${result.bonusPoints.toFixed(1)}`); + if (result.bonusModifiers.length > 0) { + result.bonusModifiers.forEach(mod => { + console.log(`[AudiobookSearch] - ${mod.reason}: +${mod.points.toFixed(1)}`); + }); + } + console.log(`[AudiobookSearch] `); + console.log(`[AudiobookSearch] Final Score: ${result.finalScore.toFixed(1)}`); if (result.breakdown.notes.length > 0) { console.log(`[AudiobookSearch] Notes: ${result.breakdown.notes.join(', ')}`); } diff --git a/src/app/api/requests/[id]/interactive-search/route.ts b/src/app/api/requests/[id]/interactive-search/route.ts index 6fbc186..9cafa88 100644 --- a/src/app/api/requests/[id]/interactive-search/route.ts +++ b/src/app/api/requests/[id]/interactive-search/route.ts @@ -82,6 +82,15 @@ export async function POST( ); } + // Build indexer priorities map (indexerId -> priority 1-25, default 10) + const indexerPriorities = new Map( + indexersConfig.map((indexer: any) => [indexer.id, indexer.priority ?? 10]) + ); + + // Get flag configurations + const flagConfigStr = await configService.get('indexer_flag_config'); + const flagConfigs = flagConfigStr ? JSON.parse(flagConfigStr) : []; + // Search Prowlarr for torrents - ONLY enabled indexers const prowlarr = await getProwlarrService(); // Use custom title if provided, otherwise use audiobook's title @@ -107,17 +116,28 @@ export async function POST( }); } - // Rank torrents using the ranking algorithm + // Rank torrents using the ranking algorithm with indexer priorities and flag configs // Always use the audiobook's title/author for ranking (not custom search query) const rankedResults = rankTorrents(results, { title: requestRecord.audiobook.title, author: requestRecord.audiobook.author, - }); + }, indexerPriorities, flagConfigs); - // Filter out results below minimum score threshold (50/100) - const filteredResults = rankedResults.filter(result => result.score >= 50); + // Dual threshold filtering: + // 1. Base score must be >= 50 (quality minimum) + // 2. Final score must be >= 50 (not disqualified by negative bonuses) + const filteredResults = rankedResults.filter(result => + result.score >= 50 && result.finalScore >= 50 + ); - console.log(`[InteractiveSearch] Ranked ${rankedResults.length} results, ${filteredResults.length} above threshold (50/100)`); + const disqualifiedByNegativeBonus = rankedResults.filter(result => + result.score >= 50 && result.finalScore < 50 + ).length; + + console.log(`[InteractiveSearch] Ranked ${rankedResults.length} results, ${filteredResults.length} above threshold (50/100 base + final)`); + if (disqualifiedByNegativeBonus > 0) { + console.log(`[InteractiveSearch] ${disqualifiedByNegativeBonus} torrents disqualified by negative flag bonuses`); + } // Log top 3 results with detailed score breakdown for debugging const top3 = filteredResults.slice(0, 3); @@ -130,12 +150,22 @@ export async function POST( console.log(`[InteractiveSearch] --------------------------------------------------------`); top3.forEach((result, index) => { console.log(`[InteractiveSearch] ${index + 1}. "${result.title}"`); - console.log(`[InteractiveSearch] Indexer: ${result.indexer}`); - console.log(`[InteractiveSearch] Total Score: ${result.score.toFixed(1)}/100`); + console.log(`[InteractiveSearch] Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`); + console.log(`[InteractiveSearch] `); + console.log(`[InteractiveSearch] Base Score: ${result.score.toFixed(1)}/100`); console.log(`[InteractiveSearch] - Title/Author Match: ${result.breakdown.matchScore.toFixed(1)}/50`); console.log(`[InteractiveSearch] - Format Quality: ${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`); console.log(`[InteractiveSearch] - Seeder Count: ${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders} seeders)`); console.log(`[InteractiveSearch] - Size Score: ${result.breakdown.sizeScore.toFixed(1)}/10 (${(result.size / (1024 ** 3)).toFixed(2)} GB)`); + console.log(`[InteractiveSearch] `); + console.log(`[InteractiveSearch] Bonus Points: +${result.bonusPoints.toFixed(1)}`); + if (result.bonusModifiers.length > 0) { + result.bonusModifiers.forEach(mod => { + console.log(`[InteractiveSearch] - ${mod.reason}: +${mod.points.toFixed(1)}`); + }); + } + console.log(`[InteractiveSearch] `); + console.log(`[InteractiveSearch] Final Score: ${result.finalScore.toFixed(1)}`); if (result.breakdown.notes.length > 0) { console.log(`[InteractiveSearch] Notes: ${result.breakdown.notes.join(', ')}`); } diff --git a/src/app/api/setup/complete/route.ts b/src/app/api/setup/complete/route.ts index e904891..7fc9026 100644 --- a/src/app/api/setup/complete/route.ts +++ b/src/app/api/setup/complete/route.ts @@ -356,6 +356,15 @@ export async function POST(request: NextRequest) { create: { key: 'download_client_password', value: downloadClient.password }, }); + await prisma.configuration.upsert({ + where: { key: 'download_client_disable_ssl_verify' }, + update: { value: downloadClient.disableSSLVerify ? 'true' : 'false' }, + create: { + key: 'download_client_disable_ssl_verify', + value: downloadClient.disableSSLVerify ? 'true' : 'false', + }, + }); + // Remote path mapping configuration await prisma.configuration.upsert({ where: { key: 'download_client_remote_path_mapping_enabled' }, diff --git a/src/app/api/setup/test-download-client/route.ts b/src/app/api/setup/test-download-client/route.ts index 2bd8b07..684f2fe 100644 --- a/src/app/api/setup/test-download-client/route.ts +++ b/src/app/api/setup/test-download-client/route.ts @@ -8,7 +8,7 @@ import { QBittorrentService } from '@/lib/integrations/qbittorrent.service'; export async function POST(request: NextRequest) { try { - const { type, url, username, password } = await request.json(); + const { type, url, username, password, disableSSLVerify } = await request.json(); if (!type || !url || !username || !password) { return NextResponse.json( @@ -28,7 +28,8 @@ export async function POST(request: NextRequest) { const version = await QBittorrentService.testConnectionWithCredentials( url, username, - password + password, + disableSSLVerify || false ); return NextResponse.json({ diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index bf73fdd..103683f 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -808,30 +808,6 @@ function LoginContent() { )}
- - {/* Footer info */} -
-

- Powered by{' '} - - Plex - - {' '}&{' '} - - Audible - -

-
diff --git a/src/app/page.tsx b/src/app/page.tsx index cd29a75..083945b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -19,6 +19,7 @@ export default function HomePage() { // Refs for auto-scrolling to section tops const popularSectionRef = useRef(null); const newReleasesSectionRef = useRef(null); + const footerRef = useRef(null); const { audiobooks: popular, @@ -139,30 +140,10 @@ export default function HomePage() { {/* Footer */} -