mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
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.
This commit is contained in:
@@ -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<PlexLibrary[]>([]);
|
||||
const [absLibraries, setAbsLibraries] = useState<ABSLibrary[]>([]);
|
||||
const [indexers, setIndexers] = useState<IndexerConfig[]>([]);
|
||||
const [flagConfigs, setFlagConfigs] = useState<IndexerFlagConfig[]>([]);
|
||||
const [pendingUsers, setPendingUsers] = useState<PendingUser[]>([]);
|
||||
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() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Flag Configuration Section */}
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
Indexer Flag Configuration (Optional)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Configure score bonuses or penalties for indexer flags like "Freeleech".
|
||||
These modifiers apply universally across all indexers and affect final torrent ranking.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{flagConfigs.length > 0 && (
|
||||
<div className="space-y-3 mb-4">
|
||||
{flagConfigs.map((config, index) => (
|
||||
<FlagConfigRow
|
||||
key={index}
|
||||
config={config}
|
||||
onChange={(updated) => {
|
||||
const newConfigs = [...flagConfigs];
|
||||
newConfigs[index] = updated;
|
||||
setFlagConfigs(newConfigs);
|
||||
}}
|
||||
onRemove={() => {
|
||||
setFlagConfigs(flagConfigs.filter((_, i) => i !== index));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFlagConfigs([...flagConfigs, { name: '', modifier: 0 }]);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
+ Add Flag Rule
|
||||
</Button>
|
||||
|
||||
{flagConfigs.length === 0 && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-3 italic">
|
||||
No flag rules configured. Flag bonuses/penalties are optional.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1551,6 +1605,42 @@ export default function AdminSettings() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* SSL Verification Toggle */}
|
||||
{settings.downloadClient.url.startsWith('https') && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4 border border-yellow-200 dark:border-yellow-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-ssl-verify"
|
||||
checked={settings.downloadClient.disableSSLVerify}
|
||||
onChange={(e) => {
|
||||
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"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label
|
||||
htmlFor="disable-ssl-verify"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100 cursor-pointer"
|
||||
>
|
||||
Disable SSL Certificate Verification
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Enable this if you're using a self-signed certificate or getting SSL errors.
|
||||
<span className="text-yellow-700 dark:text-yellow-500 font-medium"> ⚠️ Only use on trusted private networks.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remote Path Mapping */}
|
||||
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-start gap-4">
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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<number, SavedIndexerConfig>(
|
||||
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,
|
||||
|
||||
@@ -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') || '',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,6 +55,15 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Build indexer priorities map (indexerId -> priority 1-25, default 10)
|
||||
const indexerPriorities = new Map<number, number>(
|
||||
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(', ')}`);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,15 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
// Build indexer priorities map (indexerId -> priority 1-25, default 10)
|
||||
const indexerPriorities = new Map<number, number>(
|
||||
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(', ')}`);
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -808,30 +808,6 @@ function LoginContent() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer info */}
|
||||
<div className="mt-8 text-center text-sm text-gray-500">
|
||||
<p>
|
||||
Powered by{' '}
|
||||
<a
|
||||
href="https://www.plex.tv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-400 hover:text-orange-300 transition-colors"
|
||||
>
|
||||
Plex
|
||||
</a>
|
||||
{' '}&{' '}
|
||||
<a
|
||||
href="https://www.audible.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-400 hover:text-orange-300 transition-colors"
|
||||
>
|
||||
Audible
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
+4
-21
@@ -19,6 +19,7 @@ export default function HomePage() {
|
||||
// Refs for auto-scrolling to section tops
|
||||
const popularSectionRef = useRef<HTMLElement>(null);
|
||||
const newReleasesSectionRef = useRef<HTMLElement>(null);
|
||||
const footerRef = useRef<HTMLElement>(null);
|
||||
|
||||
const {
|
||||
audiobooks: popular,
|
||||
@@ -139,30 +140,10 @@ export default function HomePage() {
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16">
|
||||
<footer ref={footerRef} className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16">
|
||||
<div className="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>ReadMeABook - Audiobook Library Management System</p>
|
||||
<p className="mt-1">
|
||||
Powered by{' '}
|
||||
<a
|
||||
href="https://www.plex.tv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
Plex
|
||||
</a>
|
||||
{' '}&{' '}
|
||||
<a
|
||||
href="https://www.audible.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
Audible
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -173,6 +154,7 @@ export default function HomePage() {
|
||||
totalPages={popularTotalPages}
|
||||
onPageChange={handlePopularPageChange}
|
||||
sectionRef={popularSectionRef}
|
||||
footerRef={footerRef}
|
||||
label="Popular Audiobooks"
|
||||
/>
|
||||
<StickyPagination
|
||||
@@ -180,6 +162,7 @@ export default function HomePage() {
|
||||
totalPages={newReleasesTotalPages}
|
||||
onPageChange={handleNewReleasesPageChange}
|
||||
sectionRef={newReleasesSectionRef}
|
||||
footerRef={footerRef}
|
||||
label="New Releases"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -77,6 +77,7 @@ interface SetupState {
|
||||
downloadClientUrl: string;
|
||||
downloadClientUsername: string;
|
||||
downloadClientPassword: string;
|
||||
disableSSLVerify: boolean;
|
||||
remotePathMappingEnabled: boolean;
|
||||
remotePath: string;
|
||||
localPath: string;
|
||||
@@ -145,6 +146,7 @@ export default function SetupWizard() {
|
||||
downloadClientUrl: '',
|
||||
downloadClientUsername: 'admin',
|
||||
downloadClientPassword: '',
|
||||
disableSSLVerify: false,
|
||||
remotePathMappingEnabled: false,
|
||||
remotePath: '',
|
||||
localPath: '',
|
||||
@@ -217,6 +219,7 @@ export default function SetupWizard() {
|
||||
url: state.downloadClientUrl,
|
||||
username: state.downloadClientUsername,
|
||||
password: state.downloadClientPassword,
|
||||
disableSSLVerify: state.disableSSLVerify,
|
||||
remotePathMappingEnabled: state.remotePathMappingEnabled,
|
||||
remotePath: state.remotePath,
|
||||
localPath: state.localPath,
|
||||
@@ -503,6 +506,7 @@ export default function SetupWizard() {
|
||||
downloadClientUrl={state.downloadClientUrl}
|
||||
downloadClientUsername={state.downloadClientUsername}
|
||||
downloadClientPassword={state.downloadClientPassword}
|
||||
disableSSLVerify={state.disableSSLVerify}
|
||||
remotePathMappingEnabled={state.remotePathMappingEnabled}
|
||||
remotePath={state.remotePath}
|
||||
localPath={state.localPath}
|
||||
|
||||
@@ -14,6 +14,7 @@ interface DownloadClientStepProps {
|
||||
downloadClientUrl: string;
|
||||
downloadClientUsername: string;
|
||||
downloadClientPassword: string;
|
||||
disableSSLVerify: boolean;
|
||||
remotePathMappingEnabled: boolean;
|
||||
remotePath: string;
|
||||
localPath: string;
|
||||
@@ -27,6 +28,7 @@ export function DownloadClientStep({
|
||||
downloadClientUrl,
|
||||
downloadClientUsername,
|
||||
downloadClientPassword,
|
||||
disableSSLVerify,
|
||||
remotePathMappingEnabled,
|
||||
remotePath,
|
||||
localPath,
|
||||
@@ -54,6 +56,7 @@ export function DownloadClientStep({
|
||||
url: downloadClientUrl,
|
||||
username: downloadClientUsername,
|
||||
password: downloadClientPassword,
|
||||
disableSSLVerify,
|
||||
remotePathMappingEnabled,
|
||||
remotePath,
|
||||
localPath,
|
||||
@@ -185,6 +188,33 @@ export function DownloadClientStep({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* SSL Verification Toggle */}
|
||||
{downloadClientUrl.startsWith('https') && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4 border border-yellow-200 dark:border-yellow-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-ssl-verify-setup"
|
||||
checked={disableSSLVerify}
|
||||
onChange={(e) => onUpdate('disableSSLVerify', e.target.checked)}
|
||||
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"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label
|
||||
htmlFor="disable-ssl-verify-setup"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100 cursor-pointer"
|
||||
>
|
||||
Disable SSL Certificate Verification
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Enable this if you're using a self-signed certificate or getting SSL errors.
|
||||
<span className="text-yellow-700 dark:text-yellow-500 font-medium"> ⚠️ Only use on trusted private networks.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remote Path Mapping */}
|
||||
<div className="mt-4 bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-start gap-4">
|
||||
|
||||
Reference in New Issue
Block a user