feat: enhance MusicPlayer and SocketProvider with playlist management and real-time updates

This commit is contained in:
JSC
2025-07-07 20:51:46 +02:00
parent 8012a53235
commit 2230fa32e5
4 changed files with 246 additions and 181 deletions

View File

@@ -47,7 +47,8 @@ export function MusicPlayer() {
const progressBarRef = useRef<HTMLDivElement>(null)
if (!currentTrack) {
// Show player if there's a playlist, even if no current track is playing
if (playlist.length === 0) {
return null
}
@@ -98,7 +99,7 @@ export function MusicPlayer() {
return (
<Card className="fixed bottom-4 right-4 w-80 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border shadow-lg z-50">
{/* Thumbnail */}
{currentTrack.thumbnail && (
{currentTrack?.thumbnail && (
<div className="relative h-32 w-full overflow-hidden rounded-t-lg">
<img
src={currentTrack.thumbnail}
@@ -122,9 +123,9 @@ export function MusicPlayer() {
{/* Track info */}
<div className="space-y-1">
<h3 className="font-medium text-sm leading-tight line-clamp-1">
{currentTrack.title}
{currentTrack?.title || 'No track selected'}
</h3>
{currentTrack.artist && (
{currentTrack?.artist && (
<p className="text-xs text-muted-foreground line-clamp-1">
{currentTrack.artist}
</p>
@@ -262,7 +263,7 @@ export function MusicPlayer() {
{/* Main player area */}
<div className="flex-1 flex flex-col items-center justify-center p-8">
{/* Large thumbnail */}
{currentTrack.thumbnail && (
{currentTrack?.thumbnail && (
<div className="w-80 h-80 rounded-lg overflow-hidden mb-6 shadow-lg">
<img
src={currentTrack.thumbnail}
@@ -274,8 +275,8 @@ export function MusicPlayer() {
{/* Track info */}
<div className="text-center mb-6">
<h1 className="text-2xl font-bold mb-2">{currentTrack.title}</h1>
{currentTrack.artist && (
<h1 className="text-2xl font-bold mb-2">{currentTrack?.title || 'No track selected'}</h1>
{currentTrack?.artist && (
<p className="text-lg text-muted-foreground">{currentTrack.artist}</p>
)}
</div>

View File

@@ -21,6 +21,7 @@ export function NavPlan({ user }: NavPlanProps) {
if (!socket || !isConnected) return
const handleCreditsChanged = (data: { credits: number }) => {
console.log('WAAAAAAZAAAA')
setCredits(data.credits)
}

View File

@@ -1,4 +1,6 @@
import { createContext, useContext, useState, useRef, useEffect, type ReactNode } from 'react'
import { useSocket } from './SocketContext'
import { apiService } from '@/services/api'
export interface Track {
id: string
@@ -41,6 +43,7 @@ interface MusicPlayerContextType {
nextTrack: () => void
previousTrack: () => void
playTrack: (trackIndex: number) => void
loadPlaylist: (playlistId: number) => void
addToPlaylist: (track: Track) => void
removeFromPlaylist: (trackId: string) => void
clearPlaylist: () => void
@@ -63,232 +66,233 @@ interface MusicPlayerProviderProps {
}
export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) {
const audioRef = useRef<HTMLAudioElement | null>(null)
const { socket } = useSocket()
// Playback state
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [volume, setVolumeState] = useState(0.8)
const [volume, setVolumeState] = useState(80)
const [isMuted, setIsMuted] = useState(false)
const [playMode, setPlayMode] = useState<PlayMode>('continuous')
const [playMode, setPlayModeState] = useState<PlayMode>('continuous')
// Playlist state
const [currentTrackIndex, setCurrentTrackIndex] = useState(0)
const [playlist, setPlaylist] = useState<Track[]>([
{
id: '1',
title: 'Cheryl Lynn - Got To Be Real',
artist: 'Cheryl Lynn',
duration: 240,
thumbnail: '/api/sounds/stream/thumbnails/Cheryl Lynn - Got To Be Real Official Audio_fI569nw0YUQ.webp',
url: '/api/sounds/stream/Cheryl Lynn - Got To Be Real Official Audio_fI569nw0YUQ.opus'
},
{
id: '2',
title: 'The Whispers - And The Beat Goes On',
artist: 'The Whispers',
duration: 280,
thumbnail: '/api/sounds/stream/thumbnails/The Whispers - And The Beat Goes On Official Video_pEmX5HR9ZxU.jpg',
url: '/api/sounds/stream/The Whispers - And The Beat Goes On Official Video_pEmX5HR9ZxU.opus'
},
{
id: '3',
title: 'OLD RAP VS NEW RAP',
artist: 'Mister V, Jhon Rachid, Maskey',
duration: 320,
thumbnail: '/api/sounds/stream/thumbnails/OLD RAP VS NEW RAP Ft Mister V Jhon Rachid Maskey _PAFYcOFE3DY.webp',
url: '/api/sounds/stream/OLD RAP VS NEW RAP Ft Mister V Jhon Rachid Maskey _PAFYcOFE3DY.opus'
}
])
const [playlist, setPlaylist] = useState<Track[]>([])
const [currentPlaylistId, setCurrentPlaylistId] = useState<number | null>(null)
const [currentTrack, setCurrentTrack] = useState<Track | null>(null)
// UI state
const [isMinimized, setIsMinimized] = useState(true)
const [showPlaylist, setShowPlaylist] = useState(false)
const currentTrack = playlist[currentTrackIndex] || null
// Initialize audio element
// Fetch initial player state on mount
useEffect(() => {
audioRef.current = new Audio()
const fetchInitialState = async () => {
try {
console.log('🎵 MusicPlayerContext: Fetching initial player state...')
const response = await apiService.get('/api/player/state')
const state = await response.json()
const audio = audioRef.current
console.log('🎵 MusicPlayerContext: Initial state received', {
playlist_id: state.playlist_id,
is_playing: state.is_playing,
current_time: state.current_time,
duration: state.duration,
playlist_length: state.playlist?.length
})
const handleTimeUpdate = () => {
setCurrentTime(audio.currentTime)
// If no playlist is loaded, try to load the main playlist
if (!state.playlist_id) {
console.log('🎵 MusicPlayerContext: No playlist loaded, attempting to load main playlist...')
try {
await apiService.post('/api/player/load-main-playlist')
// Fetch state again after loading main playlist
const newResponse = await apiService.get('/api/player/state')
const newState = await newResponse.json()
console.log('🎵 MusicPlayerContext: Main playlist loaded, new state:', {
playlist_id: newState.playlist_id,
playlist_length: newState.playlist?.length
})
state = newState
} catch (loadError) {
console.warn('🎵 MusicPlayerContext: Failed to load main playlist:', loadError)
}
}
const handleLoadedMetadata = () => {
setDuration(audio.duration)
// Update all state from backend
console.log('🎵 MusicPlayerContext: Updating initial state in React')
setIsPlaying(state.is_playing || false)
setCurrentTime(state.current_time || 0)
setDuration(state.duration || 0)
setVolumeState(state.volume || 80)
setIsMuted(state.volume === 0)
setPlayModeState(state.play_mode || 'continuous')
setCurrentTrackIndex(state.current_track_index || 0)
setPlaylist(state.playlist || [])
setCurrentPlaylistId(state.playlist_id || null)
setCurrentTrack(state.current_track || null)
console.log('🎵 MusicPlayerContext: Initial state setup complete')
} catch (error) {
console.error('❌ MusicPlayerContext: Failed to fetch initial player state:', error)
}
}
const handleEnded = () => {
handleTrackEnd()
}
audio.addEventListener('timeupdate', handleTimeUpdate)
audio.addEventListener('loadedmetadata', handleLoadedMetadata)
audio.addEventListener('ended', handleEnded)
return () => {
audio.removeEventListener('timeupdate', handleTimeUpdate)
audio.removeEventListener('loadedmetadata', handleLoadedMetadata)
audio.removeEventListener('ended', handleEnded)
audio.pause()
}
fetchInitialState()
}, [])
// Update audio source when track changes
// Listen for real-time player updates via SocketIO
useEffect(() => {
if (audioRef.current && currentTrack) {
audioRef.current.src = currentTrack.url
audioRef.current.load()
}
}, [currentTrack])
console.log('🎵 MusicPlayerContext: Setting up SocketIO listeners', { hasSocket: !!socket });
// Update audio volume and mute state
useEffect(() => {
if (audioRef.current) {
audioRef.current.volume = isMuted ? 0 : volume
if (!socket) {
console.log('⏳ MusicPlayerContext: No socket available yet');
return;
}
}, [volume, isMuted])
const handleTrackEnd = () => {
switch (playMode) {
case 'loop-one':
if (audioRef.current) {
audioRef.current.currentTime = 0
audioRef.current.play()
const handlePlayerStateUpdate = (state: any) => {
console.log('🎵 MusicPlayerContext: Received player state update', {
is_playing: state.is_playing,
current_time: state.current_time,
duration: state.duration,
volume: state.volume,
current_track: state.current_track?.title,
playlist_length: state.playlist?.length
});
setIsPlaying(state.is_playing || false)
setCurrentTime(state.current_time || 0)
setDuration(state.duration || 0)
setVolumeState(state.volume || 80)
setIsMuted(state.volume === 0)
setPlayModeState(state.play_mode || 'continuous')
setCurrentTrackIndex(state.current_track_index || 0)
setPlaylist(state.playlist || [])
setCurrentPlaylistId(state.playlist_id || null)
setCurrentTrack(state.current_track || null)
}
break
case 'loop-playlist':
nextTrack()
break
case 'random':
playRandomTrack()
break
case 'continuous':
if (currentTrackIndex < playlist.length - 1) {
nextTrack()
console.log('🎵 MusicPlayerContext: Registering player_state_update listener');
socket.on('player_state_update', handlePlayerStateUpdate)
return () => {
console.log('🎵 MusicPlayerContext: Cleaning up SocketIO listeners');
socket.off('player_state_update', handlePlayerStateUpdate)
}
}, [socket])
const play = async () => {
try {
await apiService.post('/api/player/play')
} catch (error) {
console.error('Failed to play:', error)
}
}
const pause = async () => {
try {
await apiService.post('/api/player/pause')
} catch (error) {
console.error('Failed to pause:', error)
}
}
const stop = async () => {
try {
await apiService.post('/api/player/stop')
} catch (error) {
console.error('Failed to stop:', error)
}
}
const togglePlayPause = async () => {
if (isPlaying) {
await pause()
} else {
stop()
}
break
await play()
}
}
const play = () => {
if (audioRef.current && currentTrack) {
audioRef.current.play()
setIsPlaying(true)
const seekTo = async (time: number) => {
try {
const position = duration > 0 ? time / duration : 0
await apiService.post('/api/player/seek', { position })
} catch (error) {
console.error('Failed to seek:', error)
}
}
const pause = () => {
if (audioRef.current) {
audioRef.current.pause()
setIsPlaying(false)
const setVolume = async (newVolume: number) => {
try {
await apiService.post('/api/player/volume', { volume: newVolume })
} catch (error) {
console.error('Failed to set volume:', error)
}
}
const stop = () => {
if (audioRef.current) {
audioRef.current.pause()
audioRef.current.currentTime = 0
setIsPlaying(false)
setCurrentTime(0)
const toggleMute = async () => {
try {
const newVolume = isMuted ? 80 : 0
await apiService.post('/api/player/volume', { volume: newVolume })
} catch (error) {
console.error('Failed to toggle mute:', error)
}
}
const togglePlayPause = () => {
if (isPlaying) {
pause()
} else {
play()
const nextTrack = async () => {
try {
await apiService.post('/api/player/next')
} catch (error) {
console.error('Failed to skip to next track:', error)
}
}
const seekTo = (time: number) => {
if (audioRef.current) {
audioRef.current.currentTime = time
setCurrentTime(time)
const previousTrack = async () => {
try {
await apiService.post('/api/player/previous')
} catch (error) {
console.error('Failed to skip to previous track:', error)
}
}
const setVolume = (newVolume: number) => {
setVolumeState(newVolume)
setIsMuted(newVolume === 0)
}
const toggleMute = () => {
setIsMuted(!isMuted)
}
const nextTrack = () => {
if (playlist.length === 0) return
const nextIndex = (currentTrackIndex + 1) % playlist.length
setCurrentTrackIndex(nextIndex)
// Auto-play if currently playing
if (isPlaying) {
setTimeout(() => play(), 100)
const playTrack = async (trackIndex: number) => {
try {
await apiService.post('/api/player/play-track', { index: trackIndex })
} catch (error) {
console.error('Failed to play track:', error)
}
}
const previousTrack = () => {
if (playlist.length === 0) return
const prevIndex = currentTrackIndex === 0 ? playlist.length - 1 : currentTrackIndex - 1
setCurrentTrackIndex(prevIndex)
// Auto-play if currently playing
if (isPlaying) {
setTimeout(() => play(), 100)
}
}
const playRandomTrack = () => {
if (playlist.length <= 1) return
let randomIndex
do {
randomIndex = Math.floor(Math.random() * playlist.length)
} while (randomIndex === currentTrackIndex)
setCurrentTrackIndex(randomIndex)
setTimeout(() => play(), 100)
}
const playTrack = (trackIndex: number) => {
if (trackIndex >= 0 && trackIndex < playlist.length) {
setCurrentTrackIndex(trackIndex)
setTimeout(() => play(), 100)
const loadPlaylist = async (playlistId: number) => {
try {
await apiService.post('/api/player/playlist', { playlist_id: playlistId })
} catch (error) {
console.error('Failed to load playlist:', error)
}
}
const addToPlaylist = (track: Track) => {
setPlaylist(prev => [...prev, track])
// This would need to be implemented via API
console.log('Adding to playlist not yet implemented')
}
const removeFromPlaylist = (trackId: string) => {
setPlaylist(prev => {
const newPlaylist = prev.filter(track => track.id !== trackId)
// Adjust current track index if necessary
const removedIndex = prev.findIndex(track => track.id === trackId)
if (removedIndex !== -1 && removedIndex < currentTrackIndex) {
setCurrentTrackIndex(current => Math.max(0, current - 1))
} else if (removedIndex === currentTrackIndex && newPlaylist.length > 0) {
setCurrentTrackIndex(current => Math.min(current, newPlaylist.length - 1))
}
return newPlaylist
})
// This would need to be implemented via API
console.log('Removing from playlist not yet implemented')
}
const clearPlaylist = () => {
stop()
setPlaylist([])
setCurrentTrackIndex(0)
// This would need to be implemented via API
console.log('Clearing playlist not yet implemented')
}
const setPlayMode = async (mode: PlayMode) => {
try {
await apiService.post('/api/player/mode', { mode })
} catch (error) {
console.error('Failed to set play mode:', error)
}
}
const toggleMaximize = () => {
@@ -303,7 +307,7 @@ export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) {
// Playback state
isPlaying,
currentTime,
duration: currentTrack?.duration || 0,
duration,
volume,
isMuted,
playMode,
@@ -329,6 +333,7 @@ export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) {
nextTrack,
previousTrack,
playTrack,
loadPlaylist,
addToPlaylist,
removeFromPlaylist,
clearPlaylist,

View File

@@ -34,6 +34,8 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({ children }) => {
const { user, loading } = useAuth();
useEffect(() => {
console.log('🔌 SocketProvider: Creating socket connection');
// Create socket connection
const newSocket = io("http://localhost:5000", {
withCredentials: true, // Include cookies for authentication
@@ -42,45 +44,101 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({ children }) => {
upgrade: false, // Disable WebSocket upgrade
});
console.log('🔌 SocketProvider: Socket created', {
id: newSocket.id,
connected: newSocket.connected,
disconnected: newSocket.disconnected
});
// Set up event listeners
newSocket.on("connect", () => {
console.log('✅ SocketIO: Connected successfully', {
id: newSocket.id,
transport: newSocket.io.engine.transport.name
});
// Test if events work at all
console.log('🧪 SocketIO: Sending test event...');
newSocket.emit("test_event", { message: "Frontend test" });
// Send manual test event
console.log('🧪 SocketIO: Sending manual test event...');
newSocket.emit("manual_test", { message: "Frontend manual test" });
// Send authentication after connection
console.log('🔐 SocketIO: Sending authentication...');
newSocket.emit("authenticate", {});
});
newSocket.on("auth_success", () => {
newSocket.on("auth_success", (data) => {
console.log('🎉 SocketIO: Authentication successful', data);
setIsConnected(true);
});
newSocket.on("auth_error", () => {
newSocket.on("auth_error", (data) => {
console.error('❌ SocketIO: Authentication failed', data);
setIsConnected(false);
newSocket.disconnect();
});
newSocket.on("disconnect", () => {
newSocket.on("disconnect", (reason) => {
console.log('🔌 SocketIO: Disconnected', { reason });
setIsConnected(false);
});
newSocket.on("connect_error", () => {
newSocket.on("connect_error", (error) => {
console.error('❌ SocketIO: Connection error', error);
setIsConnected(false);
});
// Listen for player state updates
newSocket.on("player_state_update", (data) => {
console.log('🎵 SocketIO: Player state update received', data);
});
// Listen for credits updates
newSocket.on("credits_changed", (data) => {
console.log('💰 SocketIO: Credits changed', data);
});
// Listen for test response
newSocket.on("test_response", (data) => {
console.log('🧪 SocketIO: Test response received', data);
});
setSocket(newSocket);
// Clean up on unmount
return () => {
console.log('🔌 SocketProvider: Cleaning up socket');
newSocket.close();
};
}, []);
// Connect/disconnect based on authentication state
useEffect(() => {
if (!socket || loading) return;
console.log('🔄 SocketProvider: Auth state changed', {
hasSocket: !!socket,
loading,
hasUser: !!user,
userEmail: user?.email,
isConnected,
socketConnected: socket?.connected
});
if (!socket || loading) {
console.log('⏳ SocketProvider: Waiting for socket or user loading...');
return;
}
if (user && !isConnected) {
console.log('🚀 SocketProvider: User authenticated, connecting socket...');
socket.connect();
} else if (!user && isConnected) {
console.log('🔌 SocketProvider: User logged out, disconnecting socket...');
socket.disconnect();
} else {
console.log('⚡ SocketProvider: No action needed', { user: !!user, isConnected });
}
}, [socket, user, loading, isConnected]);