diff --git a/packages/backend/s3-config.json b/packages/backend/s3-config.json new file mode 100644 index 0000000..e191f05 --- /dev/null +++ b/packages/backend/s3-config.json @@ -0,0 +1,8 @@ +{ + "endpoint": "http://localhost:9000", + "region": "us-east-1", + "accessKeyId": "minioadmin", + "secretAccessKey": "minioadmin", + "bucketName": "music-files", + "useSSL": true +} \ No newline at end of file diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index baffc3d..f5f0711 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -6,7 +6,6 @@ import { PaginatedSongList } from "./components/PaginatedSongList"; import { PlaylistManager } from "./components/PlaylistManager"; import { SongDetails } from "./components/SongDetails"; import { Configuration } from "./pages/Configuration"; -import { MusicStorage } from "./pages/MusicStorage"; import { PersistentMusicPlayer } from "./components/PersistentMusicPlayer"; import { MusicPlayerProvider, useMusicPlayer } from "./contexts/MusicPlayerContext"; import { useXmlParser } from "./hooks/useXmlParser"; @@ -503,18 +502,17 @@ const RekordboxReader: React.FC = () => { Rekordbox Reader - {/* Music Storage Button */} - + /> {/* Export Library Button */} { onClick={handleExportLibrary} isDisabled={songs.length === 0} /> - - {/* Configuration Button */} - } - aria-label="Configuration" - variant="ghost" - color="gray.300" - _hover={{ color: "white", bg: "whiteAlpha.200" }} - onClick={() => navigate('/config')} - /> {/* Main Content */} } /> - } /> ([]); + const [isLoading, setIsLoading] = useState(false); + const [isSyncing, setIsSyncing] = useState(false); + + // Load music files on component mount + useEffect(() => { + loadMusicFiles(); + }, []); + + 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 + await loadMusicFiles(); + } 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 () => { @@ -104,6 +249,24 @@ export function Configuration() { Library Management + + + + Upload Music + + + + + + Music Library + + + + + + Song Matching + + @@ -156,6 +319,134 @@ export function Configuration() { + {/* 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' : ''} + + + + + + {isLoading ? ( + + Loading music files... + + ) : 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. + + + + ) : ( + + {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 */} + + + + {/* S3 Configuration Tab */}