Merge branch 'player'
Some checks failed
Frontend CI / lint (push) Failing after 5m8s
Frontend CI / build (push) Has been skipped

This commit is contained in:
JSC
2025-07-07 21:18:39 +02:00
5 changed files with 155 additions and 184 deletions

View File

@@ -23,7 +23,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Listen for refresh token expiration events // Listen for refresh token expiration events
const handleRefreshTokenExpired = () => { const handleRefreshTokenExpired = () => {
console.log('Refresh token expired, logging out user')
setUser(null) setUser(null)
} }

View File

@@ -47,7 +47,8 @@ export function MusicPlayer() {
const progressBarRef = useRef<HTMLDivElement>(null) 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 return null
} }
@@ -98,7 +99,7 @@ export function MusicPlayer() {
return ( 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"> <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 */} {/* Thumbnail */}
{currentTrack.thumbnail && ( {currentTrack?.thumbnail && (
<div className="relative h-32 w-full overflow-hidden rounded-t-lg"> <div className="relative h-32 w-full overflow-hidden rounded-t-lg">
<img <img
src={currentTrack.thumbnail} src={currentTrack.thumbnail}
@@ -122,9 +123,9 @@ export function MusicPlayer() {
{/* Track info */} {/* Track info */}
<div className="space-y-1"> <div className="space-y-1">
<h3 className="font-medium text-sm leading-tight line-clamp-1"> <h3 className="font-medium text-sm leading-tight line-clamp-1">
{currentTrack.title} {currentTrack?.title || 'No track selected'}
</h3> </h3>
{currentTrack.artist && ( {currentTrack?.artist && (
<p className="text-xs text-muted-foreground line-clamp-1"> <p className="text-xs text-muted-foreground line-clamp-1">
{currentTrack.artist} {currentTrack.artist}
</p> </p>
@@ -262,7 +263,7 @@ export function MusicPlayer() {
{/* Main player area */} {/* Main player area */}
<div className="flex-1 flex flex-col items-center justify-center p-8"> <div className="flex-1 flex flex-col items-center justify-center p-8">
{/* Large thumbnail */} {/* Large thumbnail */}
{currentTrack.thumbnail && ( {currentTrack?.thumbnail && (
<div className="w-80 h-80 rounded-lg overflow-hidden mb-6 shadow-lg"> <div className="w-80 h-80 rounded-lg overflow-hidden mb-6 shadow-lg">
<img <img
src={currentTrack.thumbnail} src={currentTrack.thumbnail}
@@ -274,8 +275,8 @@ export function MusicPlayer() {
{/* Track info */} {/* Track info */}
<div className="text-center mb-6"> <div className="text-center mb-6">
<h1 className="text-2xl font-bold mb-2">{currentTrack.title}</h1> <h1 className="text-2xl font-bold mb-2">{currentTrack?.title || 'No track selected'}</h1>
{currentTrack.artist && ( {currentTrack?.artist && (
<p className="text-lg text-muted-foreground">{currentTrack.artist}</p> <p className="text-lg text-muted-foreground">{currentTrack.artist}</p>
)} )}
</div> </div>

View File

@@ -1,4 +1,6 @@
import { createContext, useContext, useState, useRef, useEffect, type ReactNode } from 'react' import { createContext, useContext, useState, useRef, useEffect, type ReactNode } from 'react'
import { useSocket } from './SocketContext'
import { apiService } from '@/services/api'
export interface Track { export interface Track {
id: string id: string
@@ -41,6 +43,7 @@ interface MusicPlayerContextType {
nextTrack: () => void nextTrack: () => void
previousTrack: () => void previousTrack: () => void
playTrack: (trackIndex: number) => void playTrack: (trackIndex: number) => void
loadPlaylist: (playlistId: number) => void
addToPlaylist: (track: Track) => void addToPlaylist: (track: Track) => void
removeFromPlaylist: (trackId: string) => void removeFromPlaylist: (trackId: string) => void
clearPlaylist: () => void clearPlaylist: () => void
@@ -63,232 +66,202 @@ interface MusicPlayerProviderProps {
} }
export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) { export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) {
const audioRef = useRef<HTMLAudioElement | null>(null) const { socket } = useSocket()
// Playback state // Playback state
const [isPlaying, setIsPlaying] = useState(false) const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0) const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0) const [duration, setDuration] = useState(0)
const [volume, setVolumeState] = useState(0.8) const [volume, setVolumeState] = useState(80)
const [isMuted, setIsMuted] = useState(false) const [isMuted, setIsMuted] = useState(false)
const [playMode, setPlayMode] = useState<PlayMode>('continuous') const [playMode, setPlayModeState] = useState<PlayMode>('continuous')
// Playlist state // Playlist state
const [currentTrackIndex, setCurrentTrackIndex] = useState(0) const [currentTrackIndex, setCurrentTrackIndex] = useState(0)
const [playlist, setPlaylist] = useState<Track[]>([ const [playlist, setPlaylist] = useState<Track[]>([])
{ const [currentPlaylistId, setCurrentPlaylistId] = useState<number | null>(null)
id: '1', const [currentTrack, setCurrentTrack] = useState<Track | null>(null)
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'
}
])
// UI state // UI state
const [isMinimized, setIsMinimized] = useState(true) const [isMinimized, setIsMinimized] = useState(true)
const [showPlaylist, setShowPlaylist] = useState(false) const [showPlaylist, setShowPlaylist] = useState(false)
const currentTrack = playlist[currentTrackIndex] || null // Fetch initial player state on mount
// Initialize audio element
useEffect(() => { 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 // 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 handleTimeUpdate = () => { // Update all state from backend
setCurrentTime(audio.currentTime) 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 handleLoadedMetadata = () => { fetchInitialState()
setDuration(audio.duration)
}
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()
}
}, []) }, [])
// Update audio source when track changes // Listen for real-time player updates via SocketIO
useEffect(() => { useEffect(() => {
if (audioRef.current && currentTrack) { if (!socket) {
audioRef.current.src = currentTrack.url return;
audioRef.current.load()
} }
}, [currentTrack])
// Update audio volume and mute state const handlePlayerStateUpdate = (state: any) => {
useEffect(() => { setIsPlaying(state.is_playing || false)
if (audioRef.current) { setCurrentTime(state.current_time || 0)
audioRef.current.volume = isMuted ? 0 : volume 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 = () => { socket.on('player_state_update', handlePlayerStateUpdate)
switch (playMode) {
case 'loop-one': return () => {
if (audioRef.current) { socket.off('player_state_update', handlePlayerStateUpdate)
audioRef.current.currentTime = 0 }
audioRef.current.play() }, [socket])
}
break const play = async () => {
case 'loop-playlist': try {
nextTrack() await apiService.post('/api/player/play')
break } catch (error) {
case 'random': console.error('Failed to play:', error)
playRandomTrack()
break
case 'continuous':
if (currentTrackIndex < playlist.length - 1) {
nextTrack()
} else {
stop()
}
break
} }
} }
const play = () => { const pause = async () => {
if (audioRef.current && currentTrack) { try {
audioRef.current.play() await apiService.post('/api/player/pause')
setIsPlaying(true) } catch (error) {
console.error('Failed to pause:', error)
} }
} }
const pause = () => { const stop = async () => {
if (audioRef.current) { try {
audioRef.current.pause() await apiService.post('/api/player/stop')
setIsPlaying(false) } catch (error) {
console.error('Failed to stop:', error)
} }
} }
const stop = () => { const togglePlayPause = async () => {
if (audioRef.current) {
audioRef.current.pause()
audioRef.current.currentTime = 0
setIsPlaying(false)
setCurrentTime(0)
}
}
const togglePlayPause = () => {
if (isPlaying) { if (isPlaying) {
pause() await pause()
} else { } else {
play() await play()
} }
} }
const seekTo = (time: number) => { const seekTo = async (time: number) => {
if (audioRef.current) { try {
audioRef.current.currentTime = time const position = duration > 0 ? time / duration : 0
setCurrentTime(time) await apiService.post('/api/player/seek', { position })
} catch (error) {
console.error('Failed to seek:', error)
} }
} }
const setVolume = (newVolume: number) => { const setVolume = async (newVolume: number) => {
setVolumeState(newVolume) try {
setIsMuted(newVolume === 0) await apiService.post('/api/player/volume', { volume: newVolume })
} } catch (error) {
console.error('Failed to set volume:', error)
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 previousTrack = () => { const toggleMute = async () => {
if (playlist.length === 0) return try {
const newVolume = isMuted ? 80 : 0
const prevIndex = currentTrackIndex === 0 ? playlist.length - 1 : currentTrackIndex - 1 await apiService.post('/api/player/volume', { volume: newVolume })
setCurrentTrackIndex(prevIndex) } catch (error) {
console.error('Failed to toggle mute:', error)
// Auto-play if currently playing
if (isPlaying) {
setTimeout(() => play(), 100)
} }
} }
const playRandomTrack = () => { const nextTrack = async () => {
if (playlist.length <= 1) return try {
await apiService.post('/api/player/next')
let randomIndex } catch (error) {
do { console.error('Failed to skip to next track:', error)
randomIndex = Math.floor(Math.random() * playlist.length) }
} while (randomIndex === currentTrackIndex)
setCurrentTrackIndex(randomIndex)
setTimeout(() => play(), 100)
} }
const playTrack = (trackIndex: number) => { const previousTrack = async () => {
if (trackIndex >= 0 && trackIndex < playlist.length) { try {
setCurrentTrackIndex(trackIndex) await apiService.post('/api/player/previous')
setTimeout(() => play(), 100) } catch (error) {
console.error('Failed to skip to previous track:', error)
}
}
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 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) => { 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) => { const removeFromPlaylist = (trackId: string) => {
setPlaylist(prev => { // This would need to be implemented via API
const newPlaylist = prev.filter(track => track.id !== trackId) console.log('Removing from playlist not yet implemented')
// 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
})
} }
const clearPlaylist = () => { const clearPlaylist = () => {
stop() // This would need to be implemented via API
setPlaylist([]) console.log('Clearing playlist not yet implemented')
setCurrentTrackIndex(0) }
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 = () => { const toggleMaximize = () => {
@@ -303,7 +276,7 @@ export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) {
// Playback state // Playback state
isPlaying, isPlaying,
currentTime, currentTime,
duration: currentTrack?.duration || 0, duration,
volume, volume,
isMuted, isMuted,
playMode, playMode,
@@ -329,6 +302,7 @@ export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) {
nextTrack, nextTrack,
previousTrack, previousTrack,
playTrack, playTrack,
loadPlaylist,
addToPlaylist, addToPlaylist,
removeFromPlaylist, removeFromPlaylist,
clearPlaylist, clearPlaylist,

View File

@@ -44,6 +44,7 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({ children }) => {
// Set up event listeners // Set up event listeners
newSocket.on("connect", () => { newSocket.on("connect", () => {
// Send authentication after connection // Send authentication after connection
newSocket.emit("authenticate", {}); newSocket.emit("authenticate", {});
}); });
@@ -75,7 +76,9 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({ children }) => {
// Connect/disconnect based on authentication state // Connect/disconnect based on authentication state
useEffect(() => { useEffect(() => {
if (!socket || loading) return; if (!socket || loading) {
return;
}
if (user && !isConnected) { if (user && !isConnected) {
socket.connect(); socket.connect();

View File

@@ -82,11 +82,8 @@ class ApiService {
}) })
if (response.ok) { if (response.ok) {
console.log('Token refreshed successfully')
return true return true
} else { } else {
console.log('Token refresh failed:', response.status)
// If refresh token is also expired (401), trigger logout // If refresh token is also expired (401), trigger logout
if (response.status === 401) { if (response.status === 401) {
this.handleLogout() this.handleLogout()
@@ -103,9 +100,6 @@ class ApiService {
* Handle logout when refresh token expires * Handle logout when refresh token expires
*/ */
private handleLogout() { private handleLogout() {
// Clear any local storage or state if needed
console.log('Refresh token expired, user needs to login again')
// Dispatch a custom event that the AuthContext can listen to // Dispatch a custom event that the AuthContext can listen to
window.dispatchEvent(new CustomEvent('auth:refresh-token-expired')) window.dispatchEvent(new CustomEvent('auth:refresh-token-expired'))
} }