/** * Component: Download Client Management Container * Documentation: documentation/phase3/download-clients.md */ 'use client'; import React, { useState, useEffect } from 'react'; import { Button } from '@/components/ui/Button'; import { DownloadClientCard } from './DownloadClientCard'; import { DownloadClientModal } from './DownloadClientModal'; import { fetchWithAuth } from '@/lib/utils/api'; import { DownloadClientType, CLIENT_PROTOCOL_MAP, getClientDisplayName } from '@/lib/interfaces/download-client.interface'; interface DownloadClient { id: string; type: DownloadClientType; name: string; url: string; username?: string; password: string; enabled: boolean; disableSSLVerify: boolean; remotePathMappingEnabled: boolean; remotePath?: string; localPath?: string; category?: string; customPath?: string; postImportCategory?: string; } interface DownloadClientManagementProps { mode: 'wizard' | 'settings'; initialClients?: DownloadClient[]; onClientsChange?: (clients: DownloadClient[]) => void; downloadDir?: string; } export function DownloadClientManagement({ mode, initialClients = [], onClientsChange, downloadDir: downloadDirProp, }: DownloadClientManagementProps) { const [clients, setClients] = useState(initialClients); const [modalState, setModalState] = useState<{ isOpen: boolean; mode: 'add' | 'edit'; clientType?: DownloadClientType; currentClient?: DownloadClient; }>({ isOpen: false, mode: 'add' }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; clientId?: string; clientName?: string; }>({ isOpen: false }); const [resolvedDownloadDir, setResolvedDownloadDir] = useState(downloadDirProp || '/downloads'); // Fetch clients and download dir when in settings mode useEffect(() => { if (mode === 'settings') { fetchClients(); fetchDownloadDir(); } }, [mode]); // Sync downloadDir prop (wizard mode) useEffect(() => { if (downloadDirProp) { setResolvedDownloadDir(downloadDirProp); } }, [downloadDirProp]); const fetchClients = async () => { setLoading(true); setError(null); try { const response = await fetchWithAuth('/api/admin/settings/download-clients'); if (!response.ok) { throw new Error('Failed to fetch download clients'); } const data = await response.json(); setClients(data.clients || []); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch download clients'); } finally { setLoading(false); } }; const fetchDownloadDir = async () => { try { const response = await fetchWithAuth('/api/admin/settings'); if (response.ok) { const data = await response.json(); if (data.paths?.downloadDir) { setResolvedDownloadDir(data.paths.downloadDir); } } } catch { // Non-critical: fall back to default } }; const handleAddClient = (type: DownloadClientType) => { // Check if the protocol is already taken (regardless of enabled status) const protocol = CLIENT_PROTOCOL_MAP[type]; const existingClient = clients.find(c => CLIENT_PROTOCOL_MAP[c.type] === protocol); if (existingClient) { setError(`A ${protocol} client (${getClientDisplayName(existingClient.type)}) is already configured. Remove it first to add a different ${protocol} client.`); return; } setModalState({ isOpen: true, mode: 'add', clientType: type, }); }; const handleEditClient = (client: DownloadClient) => { setModalState({ isOpen: true, mode: 'edit', currentClient: client, }); }; const handleDeleteClient = (client: DownloadClient) => { setDeleteConfirm({ isOpen: true, clientId: client.id, clientName: client.name, }); }; const confirmDelete = async () => { if (!deleteConfirm.clientId) return; setLoading(true); setError(null); try { if (mode === 'settings') { // API call for settings mode const response = await fetchWithAuth(`/api/admin/settings/download-clients/${deleteConfirm.clientId}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete download client'); } await fetchClients(); // Refresh list } else { // Local removal for wizard mode const updated = clients.filter(c => c.id !== deleteConfirm.clientId); setClients(updated); onClientsChange?.(updated); } setDeleteConfirm({ isOpen: false }); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete download client'); } finally { setLoading(false); } }; const handleSaveClient = async (clientData: any) => { setLoading(true); setError(null); try { if (mode === 'settings') { // API call for settings mode if (modalState.mode === 'add') { const response = await fetchWithAuth('/api/admin/settings/download-clients', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(clientData), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to add download client'); } await fetchClients(); // Refresh list } else { const response = await fetchWithAuth(`/api/admin/settings/download-clients/${clientData.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(clientData), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to update download client'); } await fetchClients(); // Refresh list } } else { // Local update for wizard mode let updated: DownloadClient[]; if (modalState.mode === 'add') { const newClient = { ...clientData, id: `temp-${Date.now()}`, // Temporary ID for wizard mode }; updated = [...clients, newClient]; } else { updated = clients.map(c => (c.id === clientData.id ? { ...c, ...clientData } : c)); } setClients(updated); onClientsChange?.(updated); } setModalState({ isOpen: false, mode: 'add' }); } catch (err) { throw err; // Re-throw to let modal handle the error } finally { setLoading(false); } }; const hasTorrentClient = clients.some(c => CLIENT_PROTOCOL_MAP[c.type] === 'torrent'); const hasUsenetClient = clients.some(c => CLIENT_PROTOCOL_MAP[c.type] === 'usenet'); return (
{/* Error Display */} {error && (

{error}

)} {/* Add Client Section */}

Add Download Client

{/* qBittorrent Card */}

qBittorrent

Torrent downloads

Torrent
{hasTorrentClient ? (
Protocol already configured
) : ( )}
{/* Transmission Card */}

Transmission

Torrent downloads

Torrent
{hasTorrentClient ? (
Protocol already configured
) : ( )}
{/* Deluge Card */}

Deluge

Torrent downloads

Torrent
{hasTorrentClient ? (
Protocol already configured
) : ( )}
{/* SABnzbd Card */}

SABnzbd

Usenet/NZB downloads

Usenet
{hasUsenetClient ? (
Protocol already configured
) : ( )}
{/* NZBGet Card */}

NZBGet

Usenet/NZB downloads

Usenet
{hasUsenetClient ? (
Protocol already configured
) : ( )}
{/* Configured Clients Section */} {clients.length > 0 && (

Configured Clients

{clients.map(client => ( handleEditClient(client)} onDelete={() => handleDeleteClient(client)} /> ))}
)} {/* Empty State */} {clients.length === 0 && !loading && (

No download clients configured yet

Add at least one client to start downloading audiobooks

)} {/* Client Modal */} setModalState({ isOpen: false, mode: 'add' })} mode={modalState.mode} clientType={modalState.clientType} initialClient={modalState.currentClient} onSave={handleSaveClient} apiMode={mode} downloadDir={resolvedDownloadDir} /> {/* Delete Confirmation Modal */} {deleteConfirm.isOpen && (

Delete Download Client

Are you sure you want to delete {deleteConfirm.clientName}? This action cannot be undone.

)}
); }