import { Box, Heading, VStack, Text, Button, OrderedList, ListItem, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, useToast, IconButton, Flex, Tabs, TabList, TabPanels, Tab, TabPanel, Icon, HStack, Badge, Spinner, } from "@chakra-ui/react"; import { ChevronLeftIcon } from "@chakra-ui/icons"; import { FiDatabase, FiSettings, FiUpload, FiMusic, FiLink, FiRefreshCw, FiTrash2 } from 'react-icons/fi'; import { useNavigate } from "react-router-dom"; import { useXmlParser } from "../hooks/useXmlParser"; import { StyledFileInput } from "../components/StyledFileInput"; import { S3Configuration } from "./S3Configuration"; import { MusicUpload } from "../components/MusicUpload"; import { SongMatching } from "../components/SongMatching"; import { api } from "../services/api"; import { DuplicatesViewer } from "../components/DuplicatesViewer"; import { useState, useEffect, useMemo } from "react"; interface MusicFile { _id: string; originalName: string; title?: string; artist?: string; album?: string; duration?: number; size: number; format?: string; uploadedAt: string; songId?: any; // Reference to linked song } export function Configuration() { const { resetLibrary } = useXmlParser(); const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); const navigate = useNavigate(); // Music storage state const [musicFiles, setMusicFiles] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSyncing, setIsSyncing] = useState(false); const [hasS3Config, setHasS3Config] = useState(false); const [musicLoaded, setMusicLoaded] = useState(false); // Tabs: remember active tab across refreshes const initialTabIndex = useMemo(() => { const stored = localStorage.getItem('configTabIndex'); return stored ? parseInt(stored, 10) : 0; }, []); const [tabIndex, setTabIndex] = useState(initialTabIndex); // Tab indices (keep in sync with Tab order below) const TAB_INDEX = { LIBRARY: 0, UPLOAD: 1, MUSIC_LIBRARY: 2, MATCHING: 3, DUPLICATES: 4, S3_CONFIG: 5, } as const; // Fetch S3 config (small and safe to do on mount) useEffect(() => { const fetchS3Config = async () => { try { const res = await fetch('/api/config/s3'); if (res.ok) { const data = await res.json(); const cfg = data?.config || {}; const required = ['endpoint', 'region', 'accessKeyId', 'secretAccessKey', 'bucketName']; const present = required.every((k) => typeof cfg[k] === 'string' && cfg[k]); setHasS3Config(present); } else { setHasS3Config(false); } } catch { setHasS3Config(false); } }; fetchS3Config(); }, []); // Lazy-load: only load music files when Music Library tab becomes active useEffect(() => { if (tabIndex === TAB_INDEX.MUSIC_LIBRARY && !musicLoaded) { loadMusicFiles().then(() => setMusicLoaded(true)); } }, [tabIndex]); const loadMusicFiles = async () => { setIsLoading(true); try { const response = await fetch('/api/music/files'); if (response.ok) { const data = await response.json(); setMusicFiles(data.musicFiles || []); } else { throw new Error('Failed to load music files'); } } catch (error) { console.error('Error loading music files:', error); toast({ title: 'Error', description: 'Failed to load music files', status: 'error', duration: 3000, isClosable: true, }); } finally { setIsLoading(false); } }; const handleSyncS3 = async () => { setIsSyncing(true); try { const response = await fetch('/api/music/sync-s3', { method: 'POST', }); if (response.ok) { const data = await response.json(); toast({ title: 'S3 Sync Complete', description: `Found ${data.results.total} files, synced ${data.results.newFiles} new files`, status: 'success', duration: 5000, isClosable: true, }); // Reload music files to show the new ones if user is on Music Library tab if (tabIndex === TAB_INDEX.MUSIC_LIBRARY) { await loadMusicFiles(); } else { // Mark as not loaded so that when user opens the tab, it fetches fresh setMusicLoaded(false); } } else { throw new Error('Failed to sync S3'); } } catch (error) { console.error('Error syncing S3:', error); toast({ title: 'Error', description: 'Failed to sync S3 files', status: 'error', duration: 3000, isClosable: true, }); } finally { setIsSyncing(false); } }; const handleUploadComplete = (files: MusicFile[]) => { setMusicFiles(prev => [...files, ...prev]); toast({ title: 'Upload Complete', description: `${files.length} file${files.length !== 1 ? 's' : ''} uploaded successfully`, status: 'success', duration: 3000, isClosable: true, }); }; const handleDeleteFile = async (fileId: string) => { try { const response = await fetch(`/api/music/${fileId}`, { method: 'DELETE', }); if (response.ok) { setMusicFiles(prev => prev.filter(file => file._id !== fileId)); toast({ title: 'File Deleted', description: 'Music file deleted successfully', status: 'success', duration: 3000, isClosable: true, }); } else { throw new Error('Failed to delete file'); } } catch (error) { console.error('Error deleting file:', error); toast({ title: 'Error', description: 'Failed to delete music file', status: 'error', duration: 3000, isClosable: true, }); } }; const formatFileSize = (bytes: number): string => { const sizes = ['Bytes', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 Bytes'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; }; const formatDuration = (seconds: number): string => { if (!seconds || isNaN(seconds)) return '00:00'; const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; }; const handleResetDatabase = async () => { try { await api.resetDatabase(); await resetLibrary(); onClose(); toast({ title: "Database reset successful", description: "Your library has been cleared.", status: "success", duration: 5000, isClosable: true, }); // Navigate to homepage to show welcome modal and start fresh navigate('/'); } catch (error) { toast({ title: "Failed to reset database", description: "An error occurred while resetting the database.", status: "error", duration: 5000, isClosable: true, }); } }; return ( } aria-label="Go back" variant="ghost" onClick={() => navigate('/')} color="gray.300" _hover={{ color: "white", bg: "whiteAlpha.200" }} /> Configuration { setTabIndex(index); localStorage.setItem('configTabIndex', String(index)); }} > Library Management Upload Music Music Library Song Matching Duplicates S3 Configuration {/* Library Management Tab */} To get started with Rekordbox Reader, you'll need to export your library from Rekordbox and import it here. Open Rekordbox and go to File → Export Collection in xml format Choose a location to save your XML file Click the button below to import your Rekordbox XML file Import Library Reset Database Clear all songs and playlists from the database. This action cannot be undone. {/* Upload Music Tab */} Upload Music Files Drag and drop your music files here or click to select. Files will be uploaded to S3 storage and metadata will be automatically extracted. {/* Music Library Tab */} Music Library {musicFiles.length} file{musicFiles.length !== 1 ? 's' : ''} {tabIndex === TAB_INDEX.MUSIC_LIBRARY && isLoading ? ( Loading music files... ) : tabIndex === TAB_INDEX.MUSIC_LIBRARY && musicFiles.length === 0 ? ( No music files found in the database. Try uploading files in the Upload tab, or click "Sync S3" to find files already in your S3 bucket. {!hasS3Config && ( Configure your S3 connection in the S3 Configuration tab first. )} ) : ( {musicFiles.map((file) => ( {file.title || file.originalName} {file.format?.toUpperCase() || 'AUDIO'} {file.songId && ( Linked to Rekordbox )} {file.artist && ( {file.artist} )} {file.album && ( {file.album} )} {formatDuration(file.duration || 0)} {formatFileSize(file.size)} {file.format?.toUpperCase()} } size="sm" variant="ghost" colorScheme="red" onClick={() => handleDeleteFile(file._id)} _hover={{ bg: "red.900" }} /> ))} )} {/* Song Matching Tab */} {/* Duplicates Tab */} {/* S3 Configuration Tab */} {/* Reset Database Confirmation Modal */} Confirm Database Reset Are you sure you want to reset the database? This will: Delete all imported songs Remove all playlists Clear all custom configurations This action cannot be undone. ); }