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 */} -