diff --git a/packages/frontend/src/pages/Configuration.tsx b/packages/frontend/src/pages/Configuration.tsx index 51dea05..717fb83 100644 --- a/packages/frontend/src/pages/Configuration.tsx +++ b/packages/frontend/src/pages/Configuration.tsx @@ -35,7 +35,7 @@ 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 { DuplicatesViewer } from "../components/DuplicatesViewer.tsx"; import { useState, useEffect, useMemo } from "react"; interface MusicFile { @@ -136,35 +136,23 @@ export function Configuration() { const handleSyncS3 = async () => { setIsSyncing(true); try { - const response = await fetch('/api/music/sync-s3', { - method: 'POST', + const { jobId } = await api.startBackgroundJob('s3-sync'); + toast({ + title: 'S3 Sync Started', + description: `Job ${jobId} started. Progress will appear shortly.`, + status: 'info', + duration: 4000, + isClosable: true, }); - - 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'); + // Defer reloading; background job widget will show progress and we fetch on demand + if (tabIndex !== TAB_INDEX.MUSIC_LIBRARY) { + setMusicLoaded(false); } } catch (error) { - console.error('Error syncing S3:', error); + console.error('Error starting S3 sync:', error); toast({ title: 'Error', - description: 'Failed to sync S3 files', + description: 'Failed to start S3 sync', status: 'error', duration: 3000, isClosable: true, diff --git a/packages/frontend/src/pages/MusicStorage.tsx b/packages/frontend/src/pages/MusicStorage.tsx deleted file mode 100644 index 32c22b3..0000000 --- a/packages/frontend/src/pages/MusicStorage.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - Box, - VStack, - HStack, - Text, - Heading, - Tabs, - TabList, - TabPanels, - Tab, - TabPanel, - SimpleGrid, - Card, - CardBody, - CardHeader, - Badge, - IconButton, - useToast, - Alert, - AlertIcon, - Button, - Spinner, -} from '@chakra-ui/react'; -import { FiPlay, FiTrash2, FiMusic, FiRefreshCw } from 'react-icons/fi'; -import { MusicUpload } from '../components/MusicUpload'; -import { SongMatching } from '../components/SongMatching'; -import { useMusicPlayer } from '../contexts/MusicPlayerContext'; -import type { Song } from '../types/interfaces'; - -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 const MusicStorage: React.FC = () => { - const [musicFiles, setMusicFiles] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [isSyncing, setIsSyncing] = useState(false); - const { playSong } = useMusicPlayer(); - const toast = useToast(); - - // 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: `Successfully uploaded ${files.length} file(s)`, - 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)); - // The persistent player will handle removing the song if it was playing this file - 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, - }); - } - }; - - // Handle playing a music file from the Music Storage page - const handlePlayMusicFile = async (musicFile: MusicFile) => { - try { - // Create a Song object from the music file for the persistent player - const song: Song = { - id: musicFile._id, - title: musicFile.title || musicFile.originalName, - artist: musicFile.artist || 'Unknown Artist', - album: musicFile.album || '', - totalTime: musicFile.duration?.toString() || '0', - location: '', - s3File: { - musicFileId: musicFile._id, - s3Key: '', // This will be fetched by the persistent player - s3Url: '', - streamingUrl: '', - hasS3File: true, - }, - }; - - playSong(song); - } catch (error) { - console.error('Error playing music file:', error); - toast({ - title: 'Error', - description: 'Failed to play 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 mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; - }; - - return ( - - - - 🎵 Music Storage & Playback - - - - - - S3 Storage Feature - - Upload your music files to S3-compatible storage (MinIO) and stream them directly in the browser. - Supports MP3, WAV, FLAC, AAC, OGG, WMA, and Opus formats. - - - - - - - - Upload Music - - - Music Library - - - Song Matching - - - - - {/* Upload 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. - - - - - - - {/* 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" - colorScheme="blue" - onClick={() => handlePlayMusicFile(file)} - _hover={{ bg: "blue.700" }} - /> - } - size="sm" - variant="ghost" - colorScheme="red" - onClick={() => handleDeleteFile(file._id)} - _hover={{ bg: "red.900" }} - /> - - - - ))} - - )} - - - - {/* Song Matching Tab */} - - - - - - - {/* Persistent Music Player */} - {/* The PersistentMusicPlayer component is now managed by the global context */} - - ); -}; \ No newline at end of file