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 */}
- }
+ aria-label="Configuration"
variant="ghost"
color="gray.300"
_hover={{ color: "white", bg: "whiteAlpha.200" }}
- onClick={() => navigate('/music-storage')}
+ onClick={() => navigate('/config')}
ml="auto"
mr={2}
- >
- 🎵 Music Storage
-
+ />
{/* 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' : ''}
+
+ : }
+ size="sm"
+ variant="outline"
+ colorScheme="blue"
+ onClick={handleSyncS3}
+ isLoading={isSyncing}
+ loadingText="Syncing..."
+ _hover={{ bg: "blue.900", borderColor: "blue.400" }}
+ >
+ Sync S3
+
+
+
+
+ {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.
+
+ }
+ onClick={handleSyncS3}
+ isLoading={isSyncing}
+ loadingText="Syncing..."
+ colorScheme="blue"
+ _hover={{ bg: "blue.700" }}
+ >
+ Sync 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 */}