mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
Refactor indexer management and improve search logic
Refactors admin settings to use a new IndexersTab and card-based indexer management UI, supporting category selection and improved configuration. Updates backend and API routes to handle indexer categories, propagate ASIN for better search scoring, and group indexers by categories to optimize Prowlarr searches. Enhances documentation to clarify non-terminal request matching and auto-completion behavior. Adds new reusable components for indexer management and category selection.
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Component: Indexer Management Container
|
||||
* Documentation: documentation/frontend/components.md
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { IndexerCard } from './IndexerCard';
|
||||
import { IndexerConfigModal } from './IndexerConfigModal';
|
||||
import { AvailableIndexerRow } from './AvailableIndexerRow';
|
||||
import { DeleteConfirmModal } from './DeleteConfirmModal';
|
||||
import { fetchWithAuth } from '@/lib/utils/api';
|
||||
|
||||
interface ProwlarrIndexer {
|
||||
id: number;
|
||||
name: string;
|
||||
protocol: string;
|
||||
supportsRss: boolean;
|
||||
}
|
||||
|
||||
interface SavedIndexerConfig {
|
||||
id: number;
|
||||
name: string;
|
||||
priority: number;
|
||||
seedingTimeMinutes: number;
|
||||
rssEnabled: boolean;
|
||||
categories: number[];
|
||||
}
|
||||
|
||||
interface IndexerManagementProps {
|
||||
prowlarrUrl: string;
|
||||
prowlarrApiKey: string;
|
||||
mode: 'wizard' | 'settings';
|
||||
initialIndexers?: SavedIndexerConfig[];
|
||||
onIndexersChange?: (indexers: SavedIndexerConfig[]) => void;
|
||||
}
|
||||
|
||||
export function IndexerManagement({
|
||||
prowlarrUrl,
|
||||
prowlarrApiKey,
|
||||
mode,
|
||||
initialIndexers = [],
|
||||
onIndexersChange,
|
||||
}: IndexerManagementProps) {
|
||||
const [fetchedIndexers, setFetchedIndexers] = useState<ProwlarrIndexer[]>([]);
|
||||
const [configuredIndexers, setConfiguredIndexers] = useState<SavedIndexerConfig[]>(initialIndexers);
|
||||
const [modalState, setModalState] = useState<{
|
||||
isOpen: boolean;
|
||||
mode: 'add' | 'edit';
|
||||
indexer?: ProwlarrIndexer;
|
||||
currentConfig?: SavedIndexerConfig;
|
||||
}>({ isOpen: false, mode: 'add' });
|
||||
const [deleteModalState, setDeleteModalState] = useState<{
|
||||
isOpen: boolean;
|
||||
indexerId?: number;
|
||||
indexerName?: string;
|
||||
}>({ isOpen: false });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Sync with parent when configuredIndexers changes
|
||||
useEffect(() => {
|
||||
if (onIndexersChange) {
|
||||
onIndexersChange(configuredIndexers);
|
||||
}
|
||||
}, [configuredIndexers, onIndexersChange]);
|
||||
|
||||
// Sync with initialIndexers prop changes
|
||||
useEffect(() => {
|
||||
setConfiguredIndexers(initialIndexers);
|
||||
}, [initialIndexers]);
|
||||
|
||||
const fetchIndexers = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const endpoint = mode === 'wizard'
|
||||
? '/api/setup/test-prowlarr'
|
||||
: '/api/admin/settings/test-prowlarr';
|
||||
|
||||
// Use fetchWithAuth for settings mode (requires authentication)
|
||||
// Use plain fetch for wizard mode (no auth required)
|
||||
const response = mode === 'settings'
|
||||
? await fetchWithAuth(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url: prowlarrUrl,
|
||||
apiKey: prowlarrApiKey,
|
||||
}),
|
||||
})
|
||||
: await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url: prowlarrUrl,
|
||||
apiKey: prowlarrApiKey,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || 'Failed to fetch indexers');
|
||||
}
|
||||
|
||||
setFetchedIndexers(data.indexers || []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch indexers');
|
||||
setFetchedIndexers([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openAddModal = (indexer: ProwlarrIndexer) => {
|
||||
setModalState({
|
||||
isOpen: true,
|
||||
mode: 'add',
|
||||
indexer,
|
||||
});
|
||||
};
|
||||
|
||||
const openEditModal = (config: SavedIndexerConfig) => {
|
||||
// Find the full indexer info from fetched list
|
||||
const indexer = fetchedIndexers.find((idx) => idx.id === config.id);
|
||||
|
||||
setModalState({
|
||||
isOpen: true,
|
||||
mode: 'edit',
|
||||
indexer: indexer || {
|
||||
id: config.id,
|
||||
name: config.name,
|
||||
protocol: 'torrent', // Default fallback
|
||||
supportsRss: config.rssEnabled,
|
||||
},
|
||||
currentConfig: config,
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalState({ isOpen: false, mode: 'add' });
|
||||
};
|
||||
|
||||
const handleSave = (config: SavedIndexerConfig) => {
|
||||
if (modalState.mode === 'add') {
|
||||
// Add new indexer
|
||||
setConfiguredIndexers([...configuredIndexers, config]);
|
||||
} else {
|
||||
// Update existing indexer
|
||||
setConfiguredIndexers(
|
||||
configuredIndexers.map((idx) =>
|
||||
idx.id === config.id ? config : idx
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
const indexer = configuredIndexers.find((idx) => idx.id === id);
|
||||
if (!indexer) return;
|
||||
|
||||
setDeleteModalState({
|
||||
isOpen: true,
|
||||
indexerId: id,
|
||||
indexerName: indexer.name,
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (deleteModalState.indexerId) {
|
||||
setConfiguredIndexers(
|
||||
configuredIndexers.filter((idx) => idx.id !== deleteModalState.indexerId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const isIndexerAdded = (id: number) => {
|
||||
return configuredIndexers.some((idx) => idx.id === id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Section 1: Available Indexers */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
Available Indexers
|
||||
</h3>
|
||||
<Button
|
||||
onClick={fetchIndexers}
|
||||
loading={loading}
|
||||
variant="outline"
|
||||
disabled={!prowlarrUrl || !prowlarrApiKey}
|
||||
>
|
||||
{configuredIndexers.length > 0 || fetchedIndexers.length > 0
|
||||
? 'Refresh Indexers'
|
||||
: 'Fetch Indexers'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-sm text-red-800 dark:text-red-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fetchedIndexers.length > 0 && (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden max-h-64 overflow-y-auto">
|
||||
{fetchedIndexers.map((indexer) => (
|
||||
<AvailableIndexerRow
|
||||
key={indexer.id}
|
||||
indexer={indexer}
|
||||
isAdded={isIndexerAdded(indexer.id)}
|
||||
onAdd={() => openAddModal(indexer)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && fetchedIndexers.length === 0 && !error && (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 py-6 text-center border border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
|
||||
{prowlarrUrl && prowlarrApiKey
|
||||
? 'Click "Fetch Indexers" to load available indexers from Prowlarr.'
|
||||
: 'Enter Prowlarr URL and API key above, then fetch indexers.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section 2: Configured Indexers */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
||||
Configured Indexers ({configuredIndexers.length})
|
||||
</h3>
|
||||
|
||||
{configuredIndexers.length === 0 ? (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 py-8 text-center border border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
|
||||
<p className="mb-2">No indexers configured yet</p>
|
||||
<p className="text-xs">
|
||||
Fetch indexers from Prowlarr and click "Add" to configure them.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{configuredIndexers.map((config) => (
|
||||
<IndexerCard
|
||||
key={config.id}
|
||||
indexer={{
|
||||
id: config.id,
|
||||
name: config.name,
|
||||
protocol: 'torrent', // Will be populated correctly from fetched data
|
||||
}}
|
||||
onEdit={() => openEditModal(config)}
|
||||
onDelete={() => handleDelete(config.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Config Modal */}
|
||||
{modalState.isOpen && modalState.indexer && (
|
||||
<IndexerConfigModal
|
||||
isOpen={modalState.isOpen}
|
||||
onClose={closeModal}
|
||||
mode={modalState.mode}
|
||||
indexer={modalState.indexer}
|
||||
initialConfig={modalState.currentConfig}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<DeleteConfirmModal
|
||||
isOpen={deleteModalState.isOpen}
|
||||
onClose={() => setDeleteModalState({ isOpen: false })}
|
||||
onConfirm={confirmDelete}
|
||||
indexerName={deleteModalState.indexerName || ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user