Refactor song submission process to include YouTube metadata fetching; remove YouTube link requirement from settings for improved flexibility
This commit is contained in:
parent
77df851e95
commit
a7929cf144
@ -8,8 +8,7 @@ const LobbyScreen = () => {
|
|||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [settings, setSettings] = useState({
|
const [settings, setSettings] = useState({
|
||||||
songsPerPlayer: 4,
|
songsPerPlayer: 4,
|
||||||
maxPlayers: 10,
|
maxPlayers: 10
|
||||||
requireYoutubeLinks: true
|
|
||||||
});
|
});
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
@ -79,7 +78,6 @@ const LobbyScreen = () => {
|
|||||||
<h3><FontAwesomeIcon icon={faGear} /> Game Settings</h3>
|
<h3><FontAwesomeIcon icon={faGear} /> Game Settings</h3>
|
||||||
<p>Songs per player: {settings.songsPerPlayer}</p>
|
<p>Songs per player: {settings.songsPerPlayer}</p>
|
||||||
<p>Max players: {settings.maxPlayers}</p>
|
<p>Max players: {settings.maxPlayers}</p>
|
||||||
<p>YouTube links required: {settings.requireYoutubeLinks ? 'Yes' : 'No'}</p>
|
|
||||||
|
|
||||||
{isHost && (
|
{isHost && (
|
||||||
<button className="btn secondary" onClick={() => setShowSettings(true)}>
|
<button className="btn secondary" onClick={() => setShowSettings(true)}>
|
||||||
@ -140,19 +138,6 @@ const LobbyScreen = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group checkbox">
|
|
||||||
<label htmlFor="requireYoutubeLinks">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="requireYoutubeLinks"
|
|
||||||
name="requireYoutubeLinks"
|
|
||||||
checked={settings.requireYoutubeLinks}
|
|
||||||
onChange={handleSettingsChange}
|
|
||||||
/>
|
|
||||||
Require YouTube links
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button className="btn secondary" onClick={() => setShowSettings(false)}>
|
<button className="btn secondary" onClick={() => setShowSettings(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
|
@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import { faPlus, faTrash, faCheck, faMusic, faVideoCamera, faSearch, faSpinner, faExternalLinkAlt, faTimes } from '@fortawesome/free-solid-svg-icons';
|
import { faPlus, faTrash, faCheck, faMusic, faVideoCamera, faSearch, faSpinner, faExternalLinkAlt, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
const SongSubmissionScreen = () => {
|
const SongSubmissionScreen = () => {
|
||||||
const { lobby, currentPlayer, addSong, removeSong, setPlayerReady, searchYouTube } = useGame();
|
const { lobby, currentPlayer, addSong, removeSong, setPlayerReady, searchYouTube, getYouTubeMetadata } = useGame();
|
||||||
const [songs, setSongs] = useState([]);
|
const [songs, setSongs] = useState([]);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
const [songForm, setSongForm] = useState({
|
const [songForm, setSongForm] = useState({
|
||||||
@ -51,7 +51,7 @@ const SongSubmissionScreen = () => {
|
|||||||
if (player.songs && Array.isArray(player.songs)) {
|
if (player.songs && Array.isArray(player.songs)) {
|
||||||
console.log('Found player songs for', player.name, ':', player.songs.length);
|
console.log('Found player songs for', player.name, ':', player.songs.length);
|
||||||
console.log('Songs data:', player.songs);
|
console.log('Songs data:', player.songs);
|
||||||
setSongs(player.songs);
|
setSongs(player.songs || []);
|
||||||
} else {
|
} else {
|
||||||
console.log('No songs array for player:', player);
|
console.log('No songs array for player:', player);
|
||||||
setSongs([]);
|
setSongs([]);
|
||||||
@ -65,7 +65,7 @@ const SongSubmissionScreen = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchPlayerSongs();
|
fetchPlayerSongs();
|
||||||
// Include lobby in the dependency array to ensure this runs when the lobby state changes
|
// Important: This effect should run whenever the lobby state changes
|
||||||
}, [lobby, currentPlayer]);
|
}, [lobby, currentPlayer]);
|
||||||
|
|
||||||
// Extract video ID from YouTube URL
|
// Extract video ID from YouTube URL
|
||||||
@ -116,12 +116,34 @@ const SongSubmissionScreen = () => {
|
|||||||
const videoId = extractVideoId(value);
|
const videoId = extractVideoId(value);
|
||||||
if (videoId) {
|
if (videoId) {
|
||||||
setSongForm({ youtubeLink: value });
|
setSongForm({ youtubeLink: value });
|
||||||
setSelectedVideo({
|
|
||||||
|
// Set basic information immediately for a responsive UI
|
||||||
|
const basicVideoInfo = {
|
||||||
id: videoId,
|
id: videoId,
|
||||||
url: value,
|
url: value,
|
||||||
thumbnail: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
|
thumbnail: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
|
||||||
});
|
};
|
||||||
|
setSelectedVideo(basicVideoInfo);
|
||||||
setSearchResults([]); // Clear any existing search results
|
setSearchResults([]); // Clear any existing search results
|
||||||
|
|
||||||
|
// Fetch additional metadata from the server
|
||||||
|
try {
|
||||||
|
setIsLoadingMetadata(true);
|
||||||
|
const metadata = await getYouTubeMetadata(videoId);
|
||||||
|
if (metadata) {
|
||||||
|
// Update selected video with full metadata
|
||||||
|
setSelectedVideo({
|
||||||
|
...basicVideoInfo,
|
||||||
|
title: metadata.title || 'Unknown Title',
|
||||||
|
artist: metadata.artist || 'Unknown Artist',
|
||||||
|
thumbnail: metadata.thumbnail || basicVideoInfo.thumbnail
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching video metadata:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingMetadata(false);
|
||||||
|
}
|
||||||
} else if (value.trim()) {
|
} else if (value.trim()) {
|
||||||
// Clear any previous timeout
|
// Clear any previous timeout
|
||||||
if (searchTimeout.current) {
|
if (searchTimeout.current) {
|
||||||
@ -175,6 +197,9 @@ const SongSubmissionScreen = () => {
|
|||||||
// Use selected video data if available, otherwise fallback to search query or direct input
|
// Use selected video data if available, otherwise fallback to search query or direct input
|
||||||
let songData;
|
let songData;
|
||||||
|
|
||||||
|
// Generate a consistent ID format that will work for deletion later
|
||||||
|
const generateSongId = () => `song_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
if (selectedVideo) {
|
if (selectedVideo) {
|
||||||
// We have a selected video with full details - use all available metadata
|
// We have a selected video with full details - use all available metadata
|
||||||
songData = {
|
songData = {
|
||||||
@ -182,7 +207,7 @@ const SongSubmissionScreen = () => {
|
|||||||
title: selectedVideo.title,
|
title: selectedVideo.title,
|
||||||
artist: selectedVideo.artist,
|
artist: selectedVideo.artist,
|
||||||
thumbnail: selectedVideo.thumbnail,
|
thumbnail: selectedVideo.thumbnail,
|
||||||
id: `song_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` // Generate a unique ID
|
id: generateSongId() // Consistent ID format
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Adding song with full metadata:", songData);
|
console.log("Adding song with full metadata:", songData);
|
||||||
@ -199,7 +224,7 @@ const SongSubmissionScreen = () => {
|
|||||||
youtubeLink: youtubeLink,
|
youtubeLink: youtubeLink,
|
||||||
// Include the videoId to make server-side processing easier
|
// Include the videoId to make server-side processing easier
|
||||||
videoId: videoId,
|
videoId: videoId,
|
||||||
id: `song_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` // Generate a unique ID
|
id: generateSongId() // Consistent ID format
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Adding song with YouTube link:", songData);
|
console.log("Adding song with YouTube link:", songData);
|
||||||
@ -210,19 +235,35 @@ const SongSubmissionScreen = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the song and manually update the local songs array to ensure UI reflects the change
|
// Add the song and update UI when the server responds
|
||||||
addSong(songData);
|
// We use the Promise-based approach to wait for the server-generated ID for proper deletion
|
||||||
|
|
||||||
// Optimistically add the song to the local state to immediately reflect changes in UI
|
// Add the song with callback to update songs when the server responds
|
||||||
// Note: The server response will ultimately override this if needed
|
addSong(songData)
|
||||||
setSongs(prevSongs => [...prevSongs, songData]);
|
.then((updatedLobby) => {
|
||||||
|
console.log('Song added successfully, received updated lobby:', updatedLobby);
|
||||||
// Reset form
|
if (updatedLobby && currentPlayer) {
|
||||||
setSongForm({ youtubeLink: '' });
|
const player = updatedLobby.players.find(p => p.id === currentPlayer.id);
|
||||||
setSearchQuery('');
|
if (player && player.songs) {
|
||||||
setSearchResults([]);
|
console.log('Setting songs from addSong response:', player.songs);
|
||||||
setSelectedVideo(null);
|
setSongs([...player.songs]); // Create a new array to ensure state update
|
||||||
setIsFormVisible(false);
|
} else {
|
||||||
|
console.warn('Player or songs not found in updated lobby');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Missing lobby or currentPlayer in response');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error adding song:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setSongForm({ youtubeLink: '' });
|
||||||
|
setSearchQuery('');
|
||||||
|
setSearchResults([]);
|
||||||
|
setSelectedVideo(null);
|
||||||
|
setIsFormVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveSong = (songId) => {
|
const handleRemoveSong = (songId) => {
|
||||||
|
@ -86,7 +86,7 @@ export function GameProvider({ children }) {
|
|||||||
// Save game info when lobby is joined
|
// Save game info when lobby is joined
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lobby && currentPlayer) {
|
if (lobby && currentPlayer) {
|
||||||
localStorage.setItem('songBattleGame', JSON.stringify({
|
sessionStorage.setItem('songBattleGame', JSON.stringify({
|
||||||
lobbyId: lobby.id,
|
lobbyId: lobby.id,
|
||||||
playerName: currentPlayer.name
|
playerName: currentPlayer.name
|
||||||
}));
|
}));
|
||||||
@ -330,45 +330,52 @@ export function GameProvider({ children }) {
|
|||||||
const addSong = (song) => {
|
const addSong = (song) => {
|
||||||
if (!socket || !isConnected || !lobby) {
|
if (!socket || !isConnected || !lobby) {
|
||||||
setError('Not connected to server or no active lobby');
|
setError('Not connected to server or no active lobby');
|
||||||
return;
|
return Promise.reject('Not connected to server or no active lobby');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Attempting to add song:', song);
|
console.log('Attempting to add song:', song);
|
||||||
console.log('Current player state:', currentPlayer);
|
console.log('Current player state:', currentPlayer);
|
||||||
console.log('Current lobby state before adding song:', lobby);
|
console.log('Current lobby state before adding song:', lobby);
|
||||||
|
|
||||||
socket.emit('add_song', { song }, (response) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.log('Song addition response:', response);
|
socket.emit('add_song', { song }, (response) => {
|
||||||
if (response.error) {
|
console.log('Song addition response:', response);
|
||||||
console.error('Error adding song:', response.error);
|
if (response.error) {
|
||||||
setError(response.error);
|
console.error('Error adding song:', response.error);
|
||||||
} else if (response.lobby) {
|
setError(response.error);
|
||||||
// Log detailed lobby state for debugging
|
reject(response.error);
|
||||||
console.log('Song added successfully, full lobby response:', response.lobby);
|
} else if (response.lobby) {
|
||||||
console.log('Current player songs:', response.lobby.players.find(p => p.id === currentPlayer.id)?.songs);
|
// Log detailed lobby state for debugging
|
||||||
console.log('All players song data:',
|
console.log('Song added successfully, full lobby response:', response.lobby);
|
||||||
response.lobby.players.map(p => ({
|
console.log('Current player songs:', response.lobby.players.find(p => p.id === currentPlayer.id)?.songs);
|
||||||
name: p.name,
|
console.log('All players song data:',
|
||||||
id: p.id,
|
response.lobby.players.map(p => ({
|
||||||
songCount: p.songCount,
|
name: p.name,
|
||||||
songs: p.songs ? p.songs.map(s => ({id: s.id, title: s.title})) : 'No songs'
|
id: p.id,
|
||||||
}))
|
songCount: p.songCount,
|
||||||
);
|
songs: p.songs ? p.songs.map(s => ({id: s.id, title: s.title})) : 'No songs'
|
||||||
|
}))
|
||||||
// Force a deep clone of the lobby to ensure React detects the change
|
);
|
||||||
const updatedLobby = JSON.parse(JSON.stringify(response.lobby));
|
|
||||||
setLobby(updatedLobby);
|
// Force a deep clone of the lobby to ensure React detects the change
|
||||||
|
const updatedLobby = JSON.parse(JSON.stringify(response.lobby));
|
||||||
// Verify the state was updated correctly
|
setLobby(updatedLobby);
|
||||||
setTimeout(() => {
|
|
||||||
// This won't show the updated state immediately due to React's state update mechanism
|
// Verify the state was updated correctly
|
||||||
console.log('Lobby state after update (may not reflect immediate changes):', lobby);
|
setTimeout(() => {
|
||||||
console.log('Updated lobby that was set:', updatedLobby);
|
// This won't show the updated state immediately due to React's state update mechanism
|
||||||
}, 0);
|
console.log('Lobby state after update (may not reflect immediate changes):', lobby);
|
||||||
} else {
|
console.log('Updated lobby that was set:', updatedLobby);
|
||||||
console.error('Song addition succeeded but no lobby data was returned');
|
}, 0);
|
||||||
setError('Failed to update song list');
|
|
||||||
}
|
// Resolve with the updated lobby
|
||||||
|
resolve(updatedLobby);
|
||||||
|
} else {
|
||||||
|
console.error('Song addition succeeded but no lobby data was returned');
|
||||||
|
setError('Failed to update song list');
|
||||||
|
reject('Failed to update song list');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -391,6 +398,26 @@ export function GameProvider({ children }) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get metadata for a YouTube video by ID
|
||||||
|
const getYouTubeMetadata = (videoId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!socket || !isConnected) {
|
||||||
|
setError('Not connected to server');
|
||||||
|
reject('Not connected to server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('get_video_metadata', { videoId }, (response) => {
|
||||||
|
if (response.error) {
|
||||||
|
setError(response.error);
|
||||||
|
reject(response.error);
|
||||||
|
} else {
|
||||||
|
resolve(response.metadata);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Remove a song
|
// Remove a song
|
||||||
const removeSong = (songId) => {
|
const removeSong = (songId) => {
|
||||||
@ -456,7 +483,8 @@ export function GameProvider({ children }) {
|
|||||||
setPlayerReady,
|
setPlayerReady,
|
||||||
submitVote,
|
submitVote,
|
||||||
leaveLobby,
|
leaveLobby,
|
||||||
searchYouTube
|
searchYouTube,
|
||||||
|
getYouTubeMetadata
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</GameContext.Provider>
|
</GameContext.Provider>
|
||||||
|
@ -28,8 +28,7 @@ class GameManager {
|
|||||||
settings: {
|
settings: {
|
||||||
songsPerPlayer: 3,
|
songsPerPlayer: 3,
|
||||||
maxPlayers: 10,
|
maxPlayers: 10,
|
||||||
minPlayers: 3,
|
minPlayers: 3
|
||||||
requireYoutubeLinks: false
|
|
||||||
},
|
},
|
||||||
players: [{
|
players: [{
|
||||||
id: hostId,
|
id: hostId,
|
||||||
@ -300,15 +299,12 @@ class GameManager {
|
|||||||
return { error: 'Maximum number of songs reached' };
|
return { error: 'Maximum number of songs reached' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only require the YouTube link now
|
// If we have a YouTube link, validate it
|
||||||
if (!song.youtubeLink) {
|
if (song.youtubeLink) {
|
||||||
return { error: 'YouTube link is required' };
|
const videoId = await youtubeAPI.extractVideoId(song.youtubeLink);
|
||||||
}
|
if (!videoId) {
|
||||||
|
return { error: 'Invalid YouTube link' };
|
||||||
// If the YouTube link isn't valid, return an error
|
}
|
||||||
const videoId = await youtubeAPI.extractVideoId(song.youtubeLink);
|
|
||||||
if (!videoId) {
|
|
||||||
return { error: 'Invalid YouTube link' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle async metadata fetching
|
// Handle async metadata fetching
|
||||||
@ -329,6 +325,20 @@ class GameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get metadata for a single YouTube video
|
||||||
|
* @param {string} videoId - YouTube video ID
|
||||||
|
* @returns {Promise<Object>} Video metadata
|
||||||
|
*/
|
||||||
|
async getYouTubeVideoMetadata(videoId) {
|
||||||
|
try {
|
||||||
|
return await youtubeAPI.getVideoMetadata(videoId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting YouTube video metadata:', error);
|
||||||
|
return { error: 'Failed to get video metadata' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to add a song to a player
|
* Helper method to add a song to a player
|
||||||
* @param {number} playerIndex - Index of the player in the lobby
|
* @param {number} playerIndex - Index of the player in the lobby
|
||||||
|
@ -150,10 +150,11 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add a song
|
// Add a song
|
||||||
socket.on('add_song', ({ song }, callback) => {
|
socket.on('add_song', async ({ song }, callback) => {
|
||||||
try {
|
try {
|
||||||
console.log(`[DEBUG] Attempting to add song: ${JSON.stringify(song)}`);
|
console.log(`[DEBUG] Attempting to add song: ${JSON.stringify(song)}`);
|
||||||
const result = gameManager.addSong(socket.id, song);
|
// Fix: Properly await the result of the async addSong function
|
||||||
|
const result = await gameManager.addSong(socket.id, song);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.log(`[DEBUG] Error adding song: ${result.error}`);
|
console.log(`[DEBUG] Error adding song: ${result.error}`);
|
||||||
@ -225,6 +226,24 @@ io.on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get metadata for a single YouTube video
|
||||||
|
socket.on('get_video_metadata', async ({ videoId }, callback) => {
|
||||||
|
try {
|
||||||
|
if (!videoId || typeof videoId !== 'string') {
|
||||||
|
if (callback) callback({ error: 'Invalid video ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = await gameManager.getYouTubeVideoMetadata(videoId);
|
||||||
|
|
||||||
|
// Send response to client
|
||||||
|
if (callback) callback({ metadata });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting YouTube metadata:', error);
|
||||||
|
if (callback) callback({ error: 'Failed to get YouTube metadata' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Mark player as ready
|
// Mark player as ready
|
||||||
socket.on('player_ready', (_, callback) => {
|
socket.on('player_ready', (_, callback) => {
|
||||||
try {
|
try {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user