Refactor song submission process to include YouTube metadata fetching; remove YouTube link requirement from settings for improved flexibility

This commit is contained in:
Mathias Wagner 2025-04-24 20:04:26 +02:00
parent 77df851e95
commit a7929cf144
5 changed files with 167 additions and 84 deletions

View File

@ -8,8 +8,7 @@ const LobbyScreen = () => {
const [showSettings, setShowSettings] = useState(false);
const [settings, setSettings] = useState({
songsPerPlayer: 4,
maxPlayers: 10,
requireYoutubeLinks: true
maxPlayers: 10
});
const [copied, setCopied] = useState(false);
@ -79,7 +78,6 @@ const LobbyScreen = () => {
<h3><FontAwesomeIcon icon={faGear} /> Game Settings</h3>
<p>Songs per player: {settings.songsPerPlayer}</p>
<p>Max players: {settings.maxPlayers}</p>
<p>YouTube links required: {settings.requireYoutubeLinks ? 'Yes' : 'No'}</p>
{isHost && (
<button className="btn secondary" onClick={() => setShowSettings(true)}>
@ -140,19 +138,6 @@ const LobbyScreen = () => {
/>
</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">
<button className="btn secondary" onClick={() => setShowSettings(false)}>
Cancel

View File

@ -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';
const SongSubmissionScreen = () => {
const { lobby, currentPlayer, addSong, removeSong, setPlayerReady, searchYouTube } = useGame();
const { lobby, currentPlayer, addSong, removeSong, setPlayerReady, searchYouTube, getYouTubeMetadata } = useGame();
const [songs, setSongs] = useState([]);
const [isReady, setIsReady] = useState(false);
const [songForm, setSongForm] = useState({
@ -51,7 +51,7 @@ const SongSubmissionScreen = () => {
if (player.songs && Array.isArray(player.songs)) {
console.log('Found player songs for', player.name, ':', player.songs.length);
console.log('Songs data:', player.songs);
setSongs(player.songs);
setSongs(player.songs || []);
} else {
console.log('No songs array for player:', player);
setSongs([]);
@ -65,7 +65,7 @@ const SongSubmissionScreen = () => {
};
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]);
// Extract video ID from YouTube URL
@ -116,12 +116,34 @@ const SongSubmissionScreen = () => {
const videoId = extractVideoId(value);
if (videoId) {
setSongForm({ youtubeLink: value });
setSelectedVideo({
// Set basic information immediately for a responsive UI
const basicVideoInfo = {
id: videoId,
url: value,
thumbnail: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
});
};
setSelectedVideo(basicVideoInfo);
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()) {
// Clear any previous timeout
if (searchTimeout.current) {
@ -175,6 +197,9 @@ const SongSubmissionScreen = () => {
// Use selected video data if available, otherwise fallback to search query or direct input
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) {
// We have a selected video with full details - use all available metadata
songData = {
@ -182,7 +207,7 @@ const SongSubmissionScreen = () => {
title: selectedVideo.title,
artist: selectedVideo.artist,
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);
@ -199,7 +224,7 @@ const SongSubmissionScreen = () => {
youtubeLink: youtubeLink,
// Include the videoId to make server-side processing easier
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);
@ -210,19 +235,35 @@ const SongSubmissionScreen = () => {
}
}
// Add the song and manually update the local songs array to ensure UI reflects the change
addSong(songData);
// Add the song and update UI when the server responds
// 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
// Note: The server response will ultimately override this if needed
setSongs(prevSongs => [...prevSongs, songData]);
// Add the song with callback to update songs when the server responds
addSong(songData)
.then((updatedLobby) => {
console.log('Song added successfully, received updated lobby:', updatedLobby);
if (updatedLobby && currentPlayer) {
const player = updatedLobby.players.find(p => p.id === currentPlayer.id);
if (player && player.songs) {
console.log('Setting songs from addSong response:', player.songs);
setSongs([...player.songs]); // Create a new array to ensure state update
} 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);
// Reset form
setSongForm({ youtubeLink: '' });
setSearchQuery('');
setSearchResults([]);
setSelectedVideo(null);
setIsFormVisible(false);
};
const handleRemoveSong = (songId) => {

View File

@ -86,7 +86,7 @@ export function GameProvider({ children }) {
// Save game info when lobby is joined
useEffect(() => {
if (lobby && currentPlayer) {
localStorage.setItem('songBattleGame', JSON.stringify({
sessionStorage.setItem('songBattleGame', JSON.stringify({
lobbyId: lobby.id,
playerName: currentPlayer.name
}));
@ -330,45 +330,52 @@ export function GameProvider({ children }) {
const addSong = (song) => {
if (!socket || !isConnected || !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('Current player state:', currentPlayer);
console.log('Current lobby state before adding song:', lobby);
socket.emit('add_song', { song }, (response) => {
console.log('Song addition response:', response);
if (response.error) {
console.error('Error adding song:', response.error);
setError(response.error);
} else if (response.lobby) {
// Log detailed lobby state for debugging
console.log('Song added successfully, full lobby response:', response.lobby);
console.log('Current player songs:', response.lobby.players.find(p => p.id === currentPlayer.id)?.songs);
console.log('All players song data:',
response.lobby.players.map(p => ({
name: p.name,
id: p.id,
songCount: p.songCount,
songs: p.songs ? p.songs.map(s => ({id: s.id, title: s.title})) : 'No songs'
}))
);
return new Promise((resolve, reject) => {
socket.emit('add_song', { song }, (response) => {
console.log('Song addition response:', response);
if (response.error) {
console.error('Error adding song:', response.error);
setError(response.error);
reject(response.error);
} else if (response.lobby) {
// Log detailed lobby state for debugging
console.log('Song added successfully, full lobby response:', response.lobby);
console.log('Current player songs:', response.lobby.players.find(p => p.id === currentPlayer.id)?.songs);
console.log('All players song data:',
response.lobby.players.map(p => ({
name: p.name,
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));
setLobby(updatedLobby);
// Verify the state was updated correctly
setTimeout(() => {
// This won't show the updated state immediately due to React's state update mechanism
console.log('Lobby state after update (may not reflect immediate changes):', lobby);
console.log('Updated lobby that was set:', updatedLobby);
}, 0);
} else {
console.error('Song addition succeeded but no lobby data was returned');
setError('Failed to update song list');
}
// Verify the state was updated correctly
setTimeout(() => {
// This won't show the updated state immediately due to React's state update mechanism
console.log('Lobby state after update (may not reflect immediate changes):', lobby);
console.log('Updated lobby that was set:', updatedLobby);
}, 0);
// 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');
}
});
});
};
@ -392,6 +399,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
const removeSong = (songId) => {
if (!socket || !isConnected || !lobby) {
@ -456,7 +483,8 @@ export function GameProvider({ children }) {
setPlayerReady,
submitVote,
leaveLobby,
searchYouTube
searchYouTube,
getYouTubeMetadata
}}>
{children}
</GameContext.Provider>

View File

@ -28,8 +28,7 @@ class GameManager {
settings: {
songsPerPlayer: 3,
maxPlayers: 10,
minPlayers: 3,
requireYoutubeLinks: false
minPlayers: 3
},
players: [{
id: hostId,
@ -300,15 +299,12 @@ class GameManager {
return { error: 'Maximum number of songs reached' };
}
// We only require the YouTube link now
if (!song.youtubeLink) {
return { error: 'YouTube link is required' };
}
// If the YouTube link isn't valid, return an error
const videoId = await youtubeAPI.extractVideoId(song.youtubeLink);
if (!videoId) {
return { error: 'Invalid YouTube link' };
// If we have a YouTube link, validate it
if (song.youtubeLink) {
const videoId = await youtubeAPI.extractVideoId(song.youtubeLink);
if (!videoId) {
return { error: 'Invalid YouTube link' };
}
}
// 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
* @param {number} playerIndex - Index of the player in the lobby

View File

@ -150,10 +150,11 @@ io.on('connection', (socket) => {
});
// Add a song
socket.on('add_song', ({ song }, callback) => {
socket.on('add_song', async ({ song }, callback) => {
try {
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) {
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
socket.on('player_ready', (_, callback) => {
try {