|
|
|
|
@@ -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,202 @@ 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 {
|
|
|
|
|
const response = await apiService.get('/api/player/state')
|
|
|
|
|
let state = await response.json()
|
|
|
|
|
|
|
|
|
|
const audio = audioRef.current
|
|
|
|
|
|
|
|
|
|
const handleTimeUpdate = () => {
|
|
|
|
|
setCurrentTime(audio.currentTime)
|
|
|
|
|
// If no playlist is loaded, try to load the main playlist
|
|
|
|
|
if (!state.playlist_id) {
|
|
|
|
|
try {
|
|
|
|
|
await apiService.post('/api/player/load-main-playlist')
|
|
|
|
|
// Fetch state again after loading main playlist
|
|
|
|
|
const newResponse = await apiService.get('/api/player/state')
|
|
|
|
|
state = await newResponse.json()
|
|
|
|
|
} catch (loadError) {
|
|
|
|
|
console.warn('Failed to load main playlist:', loadError)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleLoadedMetadata = () => {
|
|
|
|
|
setDuration(audio.duration)
|
|
|
|
|
// Update all state from backend
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('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()
|
|
|
|
|
if (!socket) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}, [currentTrack])
|
|
|
|
|
|
|
|
|
|
// Update audio volume and mute state
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (audioRef.current) {
|
|
|
|
|
audioRef.current.volume = isMuted ? 0 : volume
|
|
|
|
|
const handlePlayerStateUpdate = (state: any) => {
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}, [volume, isMuted])
|
|
|
|
|
|
|
|
|
|
const handleTrackEnd = () => {
|
|
|
|
|
switch (playMode) {
|
|
|
|
|
case 'loop-one':
|
|
|
|
|
if (audioRef.current) {
|
|
|
|
|
audioRef.current.currentTime = 0
|
|
|
|
|
audioRef.current.play()
|
|
|
|
|
socket.on('player_state_update', handlePlayerStateUpdate)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
socket.off('player_state_update', handlePlayerStateUpdate)
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
case 'loop-playlist':
|
|
|
|
|
nextTrack()
|
|
|
|
|
break
|
|
|
|
|
case 'random':
|
|
|
|
|
playRandomTrack()
|
|
|
|
|
break
|
|
|
|
|
case 'continuous':
|
|
|
|
|
if (currentTrackIndex < playlist.length - 1) {
|
|
|
|
|
nextTrack()
|
|
|
|
|
}, [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 +276,7 @@ export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) {
|
|
|
|
|
// Playback state
|
|
|
|
|
isPlaying,
|
|
|
|
|
currentTime,
|
|
|
|
|
duration: currentTrack?.duration || 0,
|
|
|
|
|
duration,
|
|
|
|
|
volume,
|
|
|
|
|
isMuted,
|
|
|
|
|
playMode,
|
|
|
|
|
@@ -329,6 +302,7 @@ export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) {
|
|
|
|
|
nextTrack,
|
|
|
|
|
previousTrack,
|
|
|
|
|
playTrack,
|
|
|
|
|
loadPlaylist,
|
|
|
|
|
addToPlaylist,
|
|
|
|
|
removeFromPlaylist,
|
|
|
|
|
clearPlaylist,
|
|
|
|
|
|