diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 2dffd3d..cc89052 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -73,11 +73,41 @@ export default function RekordboxReader() { const [selectedSong, setSelectedSong] = useState(null); const [isDatabaseInitialized, setIsDatabaseInitialized] = useState(false); const [isSwitchingPlaylist, setIsSwitchingPlaylist] = useState(false); + const [currentPlayingSong, setCurrentPlayingSong] = useState(null); // Memoized song selection handler to prevent unnecessary re-renders const handleSongSelect = useCallback((song: Song) => { setSelectedSong(song); }, []); + + // Handle playing a song from the main view + const handlePlaySong = useCallback(async (song: Song) => { + try { + // Check if song has S3 file + if (song.s3File?.hasS3File) { + setCurrentPlayingSong(song); + + // Get streaming URL + const response = await fetch(`/api/music/${song.s3File.musicFileId}/stream`); + if (response.ok) { + const data = await response.json(); + + // Create audio element and play + const audio = new Audio(data.streamingUrl); + audio.play().catch(error => { + console.error('Error playing audio:', error); + }); + + // Update current playing song + setCurrentPlayingSong(song); + } else { + console.error('Failed to get streaming URL'); + } + } + } catch (error) { + console.error('Error playing song:', error); + } + }, []); // Format total duration for display const getFormattedTotalDuration = useCallback((durationSeconds?: number): string => { @@ -609,6 +639,7 @@ export default function RekordboxReader() { onSearch={searchSongs} searchQuery={searchQuery} isSwitchingPlaylist={isSwitchingPlaylist} + onPlaySong={handlePlaySong} /> diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 70eccd6..3758c78 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -17,11 +17,12 @@ import { MenuDivider, IconButton, Spinner, - + Badge, } from '@chakra-ui/react'; import { Search2Icon, ChevronDownIcon } from '@chakra-ui/icons'; import type { Song, PlaylistNode } from '../types/interfaces'; import { formatDuration, formatTotalDuration } from '../utils/formatters'; +import { FiPlay } from 'react-icons/fi'; interface PaginatedSongListProps { songs: Song[]; @@ -40,6 +41,7 @@ interface PaginatedSongListProps { searchQuery: string; depth?: number; isSwitchingPlaylist?: boolean; // New prop to indicate playlist switching + onPlaySong?: (song: Song) => void; // New prop for playing songs } // Memoized song item component to prevent unnecessary re-renders @@ -50,7 +52,8 @@ const SongItem = memo<{ onSelect: (song: Song) => void; onToggleSelection: (songId: string) => void; showCheckbox: boolean; -}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox }) => { + onPlaySong?: (song: Song) => void; +}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong }) => { // Memoize the formatted duration to prevent recalculation const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]); const handleClick = useCallback(() => { @@ -62,6 +65,17 @@ const SongItem = memo<{ onToggleSelection(song.id); }, [onToggleSelection, song.id]); + const handlePlayClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + if (onPlaySong && (song.s3File?.hasS3File || song.hasMusicFile)) { + onPlaySong(song); + } + }, [onPlaySong, song]); + + const hasMusicFile = (song: Song): boolean => { + return song.s3File?.hasS3File || song.hasMusicFile || false; + }; + return ( )} - - {song.title} - + + + {song.title} + + {hasMusicFile(song) && ( + + 🎵 + + )} + {song.artist} @@ -98,6 +119,18 @@ const SongItem = memo<{ {song.averageBpm} BPM + {hasMusicFile(song) && onPlaySong && ( + } + size="sm" + variant="ghost" + colorScheme="blue" + onClick={handlePlayClick} + ml={2} + _hover={{ bg: "blue.900" }} + /> + )} ); }); @@ -120,7 +153,8 @@ export const PaginatedSongList: React.FC = memo(({ onSearch, searchQuery, depth = 0, - isSwitchingPlaylist = false + isSwitchingPlaylist = false, + onPlaySong }) => { const [selectedSongs, setSelectedSongs] = useState>(new Set()); const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery); @@ -217,9 +251,10 @@ export const PaginatedSongList: React.FC = memo(({ onSelect={handleSongSelect} onToggleSelection={toggleSelection} showCheckbox={selectedSongs.size > 0 || depth === 0} + onPlaySong={onPlaySong} /> )); - }, [songs, selectedSongs, selectedSongId, toggleSelection, depth]); // Removed handleSongSelect since it's already memoized + }, [songs, selectedSongs, selectedSongId, toggleSelection, depth, onPlaySong]); // Removed handleSongSelect since it's already memoized // Use total playlist duration if available, otherwise calculate from current songs const totalDuration = useMemo(() => { diff --git a/packages/frontend/src/pages/MusicStorage.tsx b/packages/frontend/src/pages/MusicStorage.tsx index 989244a..7770606 100644 --- a/packages/frontend/src/pages/MusicStorage.tsx +++ b/packages/frontend/src/pages/MusicStorage.tsx @@ -276,14 +276,57 @@ export const MusicStorage: React.FC = () => { ) : ( - + {musicFiles.map((file) => ( - - - - - {file.format?.toUpperCase() || 'AUDIO'} - + + + + + + {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={() => setSelectedFile(file)} + _hover={{ bg: "blue.700" }} + /> } @@ -294,44 +337,10 @@ export const MusicStorage: React.FC = () => { _hover={{ bg: "red.900" }} /> - - - - - {file.title || file.originalName} - - {file.artist && ( - - {file.artist} - - )} - {file.album && ( - - {file.album} - - )} - - {formatDuration(file.duration || 0)} - {formatFileSize(file.size)} - - {file.songId && ( - - Linked to Rekordbox - - )} - } - size="sm" - colorScheme="blue" - onClick={() => setSelectedFile(file)} - _hover={{ bg: "blue.700" }} - /> - - - + + ))} - + )}