Implement proper youtube support
This commit is contained in:
parent
f00ca9ba7c
commit
206988f4b7
117
client/src/components/YouTubePlayer/YouTubePlayer.jsx
Normal file
117
client/src/components/YouTubePlayer/YouTubePlayer.jsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import './styles.sass';
|
||||
|
||||
const YouTubePlayer = ({
|
||||
videoId,
|
||||
autoplay = false,
|
||||
volume = 50,
|
||||
startTime = 45,
|
||||
onReady = () => {},
|
||||
onError = () => {},
|
||||
className = ''
|
||||
}) => {
|
||||
const iframeRef = useRef(null);
|
||||
const playerRef = useRef(null);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
console.log("YouTubePlayer rendering with videoId:", videoId, "volume:", volume, "startTime:", startTime);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.YT) {
|
||||
const tag = document.createElement('script');
|
||||
tag.src = 'https://www.youtube.com/iframe_api';
|
||||
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
console.log("YouTube API ready");
|
||||
};
|
||||
|
||||
const firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoId) return;
|
||||
|
||||
const createPlayer = () => {
|
||||
if (!window.YT || !window.YT.Player) {
|
||||
console.log("YouTube API not ready, waiting...");
|
||||
setTimeout(createPlayer, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerRef.current) {
|
||||
playerRef.current.destroy();
|
||||
playerRef.current = null;
|
||||
}
|
||||
|
||||
console.log("Creating YouTube player for video:", videoId);
|
||||
|
||||
try {
|
||||
playerRef.current = new window.YT.Player(iframeRef.current, {
|
||||
height: '200',
|
||||
width: '300',
|
||||
videoId: videoId,
|
||||
playerVars: {
|
||||
'playsinline': 1,
|
||||
'controls': 1,
|
||||
'showinfo': 0,
|
||||
'rel': 0,
|
||||
'autoplay': autoplay ? 1 : 0,
|
||||
'start': startTime
|
||||
},
|
||||
events: {
|
||||
'onReady': (event) => {
|
||||
console.log("YouTube player ready");
|
||||
event.target.setVolume(volume);
|
||||
if (autoplay) {
|
||||
event.target.seekTo(startTime);
|
||||
event.target.playVideo();
|
||||
}
|
||||
setIsLoaded(true);
|
||||
onReady();
|
||||
},
|
||||
'onError': (event) => {
|
||||
console.error("YouTube player error:", event);
|
||||
onError(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error creating YouTube player:", error);
|
||||
}
|
||||
};
|
||||
|
||||
createPlayer();
|
||||
|
||||
return () => {
|
||||
if (playerRef.current) {
|
||||
try {
|
||||
playerRef.current.destroy();
|
||||
playerRef.current = null;
|
||||
} catch (e) {
|
||||
console.error("Error destroying player:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [videoId, autoplay, onReady, onError, startTime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playerRef.current && isLoaded) {
|
||||
console.log("Setting YouTube volume to:", volume);
|
||||
try {
|
||||
playerRef.current.setVolume(volume);
|
||||
} catch (error) {
|
||||
console.error("Error setting volume:", error);
|
||||
}
|
||||
}
|
||||
}, [volume, isLoaded]);
|
||||
|
||||
return (
|
||||
<div className={`youtube-player-container ${className}`}>
|
||||
<div ref={iframeRef} className="youtube-embed" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default YouTubePlayer;
|
32
client/src/components/YouTubePlayer/styles.sass
Normal file
32
client/src/components/YouTubePlayer/styles.sass
Normal file
@ -0,0 +1,32 @@
|
||||
.youtube-player-container
|
||||
position: fixed
|
||||
right: 20px
|
||||
bottom: 120px
|
||||
width: 300px
|
||||
height: 200px
|
||||
z-index: 100
|
||||
|
||||
&.hidden-player
|
||||
width: 1px
|
||||
height: 1px
|
||||
opacity: 0
|
||||
overflow: hidden
|
||||
position: absolute
|
||||
left: -9999px
|
||||
top: -9999px
|
||||
|
||||
.youtube-embed
|
||||
width: 100%
|
||||
height: 100%
|
||||
border: 0
|
||||
border-radius: 8px
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5)
|
||||
|
||||
.audio-player
|
||||
width: 1px
|
||||
height: 1px
|
||||
position: absolute
|
||||
top: -9999px
|
||||
left: -9999px
|
||||
opacity: 0
|
||||
visibility: hidden
|
@ -3,7 +3,17 @@ import {SocketContext} from "@/common/contexts/SocketContext";
|
||||
import {useContext, useState, useEffect, useRef, useCallback} from "react";
|
||||
import MusicSlider from "@/pages/Game/components/MusicSlider";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faMessage, faMusic, faHeadphones, faClock, faCrown, faPaperPlane} from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faMessage,
|
||||
faMusic,
|
||||
faHeadphones,
|
||||
faClock,
|
||||
faCrown,
|
||||
faPaperPlane,
|
||||
faCheckCircle
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { fetchPlaylistSongs } from "@/services/youtubeService.js";
|
||||
import YouTubePlayer from "../../components/YouTubePlayer/YouTubePlayer";
|
||||
|
||||
export const Game = () => {
|
||||
const {send, on, socket, connected, connect} = useContext(SocketContext);
|
||||
@ -34,6 +44,11 @@ export const Game = () => {
|
||||
const messageEndRef = useRef(null);
|
||||
const timerIntervalRef = useRef(null);
|
||||
|
||||
const [allSongs, setAllSongs] = useState([]);
|
||||
const [playerVolume, setPlayerVolume] = useState(30);
|
||||
const [playbackError, setPlaybackError] = useState(null);
|
||||
const [songsLoading, setSongsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected) return;
|
||||
|
||||
@ -144,6 +159,50 @@ export const Game = () => {
|
||||
messageEndRef.current?.scrollIntoView({behavior: "smooth"});
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !socket) return;
|
||||
|
||||
const loadSongs = async () => {
|
||||
try {
|
||||
setSongsLoading(true);
|
||||
console.log("Fetching songs with active socket:", socket?.id);
|
||||
const songs = await fetchPlaylistSongs(socket);
|
||||
console.log(`Successfully loaded ${songs.length} songs`);
|
||||
setAllSongs(songs);
|
||||
} catch (error) {
|
||||
console.error("Error loading songs:", error);
|
||||
} finally {
|
||||
setSongsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadSongs();
|
||||
}, [socket, connected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allSongs.length || !currentSong || !currentSong.id) return;
|
||||
|
||||
const enhancedSong = allSongs.find(song => song.id === currentSong.id);
|
||||
if (enhancedSong && enhancedSong !== currentSong) {
|
||||
setCurrentSong(enhancedSong);
|
||||
}
|
||||
}, [allSongs, currentSong]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allSongs.length || !songOptions.length) return;
|
||||
|
||||
const enhancedOptions = songOptions.map(option => {
|
||||
if (option.id && !option.title) {
|
||||
return allSongs.find(song => song.id === option.id) || option;
|
||||
}
|
||||
return option;
|
||||
});
|
||||
|
||||
if (JSON.stringify(enhancedOptions) !== JSON.stringify(songOptions)) {
|
||||
setSongOptions(enhancedOptions);
|
||||
}
|
||||
}, [allSongs, songOptions]);
|
||||
|
||||
const handleFrequencyChange = useCallback((newFrequency) => {
|
||||
setFrequency(newFrequency);
|
||||
if (role === "composer") {
|
||||
@ -185,7 +244,27 @@ export const Game = () => {
|
||||
setTimeLeft(0);
|
||||
}, [send]);
|
||||
|
||||
// Phase-specific content rendering
|
||||
const handlePlayerReady = useCallback(() => {
|
||||
setPlaybackError(null);
|
||||
}, []);
|
||||
|
||||
const handlePlayerError = useCallback((error) => {
|
||||
console.error("YouTube playback error:", error);
|
||||
setPlaybackError("Error playing song - trying to continue...");
|
||||
}, []);
|
||||
|
||||
const togglePlayback = useCallback(() => {
|
||||
const audioElement = document.querySelector('audio');
|
||||
|
||||
if (audioElement) {
|
||||
if (audioElement.paused) {
|
||||
audioElement.play().catch(err => console.error("Play error:", err));
|
||||
} else {
|
||||
audioElement.pause();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderPhaseContent = () => {
|
||||
switch (phase) {
|
||||
case "waiting":
|
||||
@ -264,7 +343,7 @@ export const Game = () => {
|
||||
<img src={song.coverUrl} alt={song.title} />
|
||||
{selectedSong?.id === song.id && (
|
||||
<div className="selection-indicator">
|
||||
<FontAwesomeIcon icon="fa-solid fa-check-circle" />
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -285,7 +364,7 @@ export const Game = () => {
|
||||
onClick={handleSubmitGuess}
|
||||
disabled={!selectedSong}
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-paper-plane" />
|
||||
<FontAwesomeIcon icon={faPaperPlane} />
|
||||
Antwort einreichen
|
||||
</button>
|
||||
)}
|
||||
@ -429,6 +508,46 @@ export const Game = () => {
|
||||
frequency={frequency}
|
||||
/>
|
||||
)}
|
||||
|
||||
{role === "composer" && currentSong && currentSong.youtubeId && (
|
||||
<YouTubePlayer
|
||||
videoId={currentSong.youtubeId}
|
||||
autoplay={phase === "composing"}
|
||||
volume={playerVolume}
|
||||
startTime={currentSong.refrainTime || 45}
|
||||
onReady={handlePlayerReady}
|
||||
onError={handlePlayerError}
|
||||
className={phase === "composing" ? "" : "hidden-player"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{playbackError && (
|
||||
<div className="playback-error">
|
||||
{playbackError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "composing" && role === "composer" && currentSong && (
|
||||
<div className="player-controls">
|
||||
<div className="volume-controls">
|
||||
<span>Music Volume:</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={playerVolume}
|
||||
onChange={(e) => setPlayerVolume(parseInt(e.target.value))}
|
||||
className="volume-slider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{songsLoading && (
|
||||
<div className="songs-loading-indicator">
|
||||
Loading songs...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -962,3 +962,67 @@
|
||||
50%
|
||||
opacity: 1
|
||||
transform: scale(1.05)
|
||||
|
||||
.volume-controls
|
||||
position: fixed
|
||||
top: 70px
|
||||
right: 20px
|
||||
background: rgba(0, 0, 0, 0.5)
|
||||
padding: 10px 15px
|
||||
border-radius: 10px
|
||||
display: flex
|
||||
align-items: center
|
||||
z-index: 10
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3)
|
||||
|
||||
span
|
||||
color: white
|
||||
margin-right: 10px
|
||||
font-size: 14px
|
||||
|
||||
.volume-slider
|
||||
width: 100px
|
||||
height: 6px
|
||||
-webkit-appearance: none
|
||||
appearance: none
|
||||
background: rgba(255, 255, 255, 0.2)
|
||||
border-radius: 3px
|
||||
outline: none
|
||||
|
||||
&::-webkit-slider-thumb
|
||||
-webkit-appearance: none
|
||||
appearance: none
|
||||
width: 16px
|
||||
height: 16px
|
||||
border-radius: 50%
|
||||
background: $yellow
|
||||
cursor: pointer
|
||||
box-shadow: 0 0 10px rgba(255, 255, 0, 0.5)
|
||||
|
||||
&::-moz-range-thumb
|
||||
width: 16px
|
||||
height: 16px
|
||||
border-radius: 50%
|
||||
background: $yellow
|
||||
cursor: pointer
|
||||
box-shadow: 0 0 10px rgba(255, 255, 0, 0.5)
|
||||
|
||||
.playback-error
|
||||
position: fixed
|
||||
bottom: 10px
|
||||
left: 50%
|
||||
transform: translateX(-50%)
|
||||
background: rgba(255, 0, 0, 0.7)
|
||||
color: white
|
||||
padding: 8px 20px
|
||||
border-radius: 20px
|
||||
font-size: 14px
|
||||
z-index: 1000
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3)
|
||||
animation: fade-out 4s forwards
|
||||
|
||||
@keyframes fade-out
|
||||
0%, 80%
|
||||
opacity: 1
|
||||
100%
|
||||
opacity: 0
|
112
client/src/services/youtubeService.js
Normal file
112
client/src/services/youtubeService.js
Normal file
@ -0,0 +1,112 @@
|
||||
let cachedSongs = null;
|
||||
let fetchPromise = null;
|
||||
|
||||
|
||||
export const fetchPlaylistSongs = async (socketInstance) => {
|
||||
if (cachedSongs && cachedSongs.length > 0) {
|
||||
console.log("Returning cached songs:", cachedSongs.length);
|
||||
return cachedSongs;
|
||||
}
|
||||
|
||||
if (fetchPromise) {
|
||||
console.log("Request already in progress, waiting for response...");
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
console.log("Starting new fetch request for playlist songs");
|
||||
|
||||
fetchPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let socket = socketInstance;
|
||||
|
||||
if (!socket) {
|
||||
try {
|
||||
const socketModule = await import('@/common/contexts/SocketContext');
|
||||
socket = socketModule.socket;
|
||||
} catch (error) {
|
||||
console.error("Failed to import socket:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!socket) {
|
||||
throw new Error("Socket not available");
|
||||
}
|
||||
|
||||
if (!socket.connected) {
|
||||
console.warn("Socket not connected - attempting to connect");
|
||||
socket.connect();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
if (!socket.connected) {
|
||||
throw new Error("Could not connect to server");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Socket connected, requesting songs...");
|
||||
|
||||
const responsePromise = new Promise((resolve, reject) => {
|
||||
const handleResponse = (data) => {
|
||||
console.log("Received playlist-songs response");
|
||||
|
||||
if (data.error) {
|
||||
console.error("Server returned error:", data.error);
|
||||
reject(new Error(data.error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (data && data.songs && Array.isArray(data.songs) && data.songs.length > 0) {
|
||||
cachedSongs = data.songs;
|
||||
resolve(data.songs);
|
||||
} else {
|
||||
console.error("Invalid song data in response", data);
|
||||
reject(new Error("Invalid song data"));
|
||||
}
|
||||
};
|
||||
|
||||
socket.once("playlist-songs", handleResponse);
|
||||
|
||||
setTimeout(() => {
|
||||
socket.off("playlist-songs", handleResponse);
|
||||
reject(new Error("Request timed out"));
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
socket.emit("get-playlist-songs");
|
||||
|
||||
const songs = await responsePromise;
|
||||
fetchPromise = null;
|
||||
resolve(songs);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching playlist songs:", error);
|
||||
fetchPromise = null;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return fetchPromise.catch(error => {
|
||||
console.error("Failed to fetch playlist songs:", error);
|
||||
|
||||
return [{
|
||||
id: 1,
|
||||
title: "Connection Error",
|
||||
artist: "Please refresh the page",
|
||||
coverUrl: "https://place-hold.it/500x500/f00/fff?text=Error",
|
||||
youtubeId: "dQw4w9WgXcQ"
|
||||
}];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a single song by ID
|
||||
*/
|
||||
export const getSongById = async (id, socketInstance) => {
|
||||
try {
|
||||
const songs = await fetchPlaylistSongs(socketInstance);
|
||||
return songs.find(song => song.id === id) || null;
|
||||
} catch (error) {
|
||||
console.error("Error getting song by ID:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
@ -1,18 +1,5 @@
|
||||
const roomController = require('./room');
|
||||
|
||||
const SONGS = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Black Steam",
|
||||
artist: "Carrot Quest GmbH",
|
||||
coverUrl: "https://mir-s3-cdn-cf.behance.net/project_modules/1400/fe529a64193929.5aca8500ba9ab.jpg"
|
||||
},
|
||||
{id: 2, title: "Sunset Dreams", artist: "Ocean Waves", coverUrl: "https://place-hold.it/500x500/"},
|
||||
{id: 3, title: "Neon Nights", artist: "Electric Avenue", coverUrl: "https://place-hold.it/500x500/"},
|
||||
{id: 4, title: "Mountain Echo", artist: "Wild Terrain", coverUrl: "https://place-hold.it/500x500/"},
|
||||
{id: 5, title: "Urban Jungle", artist: "City Dwellers", coverUrl: "https://place-hold.it/500x500/"},
|
||||
{id: 6, title: "Cosmic Journey", artist: "Stargazers", coverUrl: "https://place-hold.it/500x500/"}
|
||||
];
|
||||
const youtubeService = require('../services/youtubeService');
|
||||
|
||||
const gameStates = {};
|
||||
|
||||
@ -44,7 +31,7 @@ const initializeGameState = (roomId) => {
|
||||
return gameStates[roomId];
|
||||
};
|
||||
|
||||
const startNewRound = (roomId) => {
|
||||
const startNewRound = async (roomId) => {
|
||||
const gameState = gameStates[roomId];
|
||||
if (!gameState) return false;
|
||||
|
||||
@ -63,9 +50,7 @@ const startNewRound = (roomId) => {
|
||||
gameState.roles[user.id] = user.id === gameState.currentComposer ? 'composer' : 'guesser';
|
||||
});
|
||||
|
||||
selectSongAndOptions(gameState);
|
||||
|
||||
return true;
|
||||
return await selectSongAndOptions(gameState);
|
||||
};
|
||||
|
||||
const determineNextComposer = (roomId, gameState, users) => {
|
||||
@ -82,16 +67,35 @@ const determineNextComposer = (roomId, gameState, users) => {
|
||||
return users[(currentIndex + 1) % users.length].id;
|
||||
};
|
||||
|
||||
const selectSongAndOptions = (gameState) => {
|
||||
gameState.selectedSong = SONGS[Math.floor(Math.random() * SONGS.length)];
|
||||
const selectSongAndOptions = async (gameState) => {
|
||||
try {
|
||||
const availableIds = await youtubeService.getAvailableSongIds();
|
||||
|
||||
const songOptions = [...SONGS].sort(() => Math.random() - 0.5).slice(0, 5);
|
||||
|
||||
if (!songOptions.includes(gameState.selectedSong)) {
|
||||
songOptions[Math.floor(Math.random() * songOptions.length)] = gameState.selectedSong;
|
||||
if (!availableIds || availableIds.length === 0) {
|
||||
console.error("No song IDs available for selection");
|
||||
return false;
|
||||
}
|
||||
|
||||
gameState.songOptions = songOptions;
|
||||
const selectedId = availableIds[Math.floor(Math.random() * availableIds.length)];
|
||||
gameState.selectedSong = { id: selectedId };
|
||||
|
||||
const optionIds = new Set([selectedId]);
|
||||
const attempts = Math.min(20, availableIds.length);
|
||||
|
||||
let count = 0;
|
||||
while (optionIds.size < 5 && count < attempts) {
|
||||
const randomId = availableIds[Math.floor(Math.random() * availableIds.length)];
|
||||
optionIds.add(randomId);
|
||||
count++;
|
||||
}
|
||||
|
||||
gameState.songOptions = Array.from(optionIds).map(id => ({ id }));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error selecting songs and options:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getTimeRemaining = (roomId) => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
const roomController = require("../controller/room");
|
||||
const gameController = require("../controller/game");
|
||||
const youtubeService = require('../services/youtubeService');
|
||||
|
||||
module.exports = (io) => (socket) => {
|
||||
let currentRoomId = null;
|
||||
@ -147,7 +148,7 @@ module.exports = (io) => (socket) => {
|
||||
currentRoomId = roomId;
|
||||
});
|
||||
|
||||
socket.on("start-game", () => {
|
||||
socket.on("start-game", async () => {
|
||||
const roomId = roomController.getUserRoom(socket.id);
|
||||
if (!roomId || !roomController.isUserHost(socket.id)) {
|
||||
return socket.emit("not-authorized");
|
||||
@ -162,10 +163,19 @@ module.exports = (io) => (socket) => {
|
||||
|
||||
if (!roomController.startGame(roomId)) return;
|
||||
gameController.initializeGameState(roomId);
|
||||
if (!gameController.startNewRound(roomId)) return;
|
||||
|
||||
try {
|
||||
const success = await gameController.startNewRound(roomId);
|
||||
if (!success) {
|
||||
return socket.emit("error", { message: "Failed to start game - could not load songs" });
|
||||
}
|
||||
|
||||
io.to(roomId).emit("game-started");
|
||||
handleRoundStart(roomId);
|
||||
} catch (error) {
|
||||
console.error("Error starting game:", error);
|
||||
socket.emit("error", { message: "Failed to start game due to an error" });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("send-message", (messageData) => {
|
||||
@ -233,7 +243,7 @@ module.exports = (io) => (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("next-round", () => {
|
||||
socket.on("next-round", async () => {
|
||||
const roomId = roomController.getUserRoom(socket.id);
|
||||
if (!roomId || !roomController.isUserHost(socket.id)) return;
|
||||
|
||||
@ -244,8 +254,16 @@ module.exports = (io) => (socket) => {
|
||||
return socket.emit("error", { message: "At least 2 players are required" });
|
||||
}
|
||||
|
||||
if (gameController.startNewRound(roomId)) {
|
||||
try {
|
||||
const success = await gameController.startNewRound(roomId);
|
||||
if (success) {
|
||||
handleRoundStart(roomId);
|
||||
} else {
|
||||
socket.emit("error", { message: "Failed to start next round" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error starting next round:", error);
|
||||
socket.emit("error", { message: "Failed to start next round due to an error" });
|
||||
}
|
||||
});
|
||||
|
||||
@ -257,4 +275,14 @@ module.exports = (io) => (socket) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("get-playlist-songs", async () => {
|
||||
try {
|
||||
const songs = await youtubeService.fetchPlaylistSongs();
|
||||
socket.emit("playlist-songs", { songs });
|
||||
} catch (error) {
|
||||
console.error("Error sending playlist songs:", error);
|
||||
socket.emit("playlist-songs", { songs: youtubeService.getDefaultSongs() });
|
||||
}
|
||||
});
|
||||
};
|
71
server/services/youtubeService.js
Normal file
71
server/services/youtubeService.js
Normal file
@ -0,0 +1,71 @@
|
||||
const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;
|
||||
const PLAYLIST_ID = "PLmXxqSJJq-yXrCPGIT2gn8b34JjOrl4Xf";
|
||||
|
||||
let cachedSongs = null;
|
||||
let lastFetchTime = 0;
|
||||
const CACHE_TTL = 3600000; // 1 hour
|
||||
|
||||
/**
|
||||
* Fetches songs from YouTube playlist and returns them
|
||||
*/
|
||||
const fetchPlaylistSongs = async () => {
|
||||
const now = Date.now();
|
||||
if (cachedSongs && cachedSongs.length > 0 && (now - lastFetchTime) < CACHE_TTL) {
|
||||
console.log("Returning cached YouTube songs:", cachedSongs.length);
|
||||
return cachedSongs;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Fetching songs from YouTube API...");
|
||||
const playlistUrl = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&maxResults=50&playlistId=${PLAYLIST_ID}&key=${YOUTUBE_API_KEY}`;
|
||||
const response = await fetch(playlistUrl);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
console.error("YouTube API error:", data.error);
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
|
||||
if (!data.items || !data.items.length) {
|
||||
console.error("No items found in YouTube playlist");
|
||||
throw new Error("No songs found in the playlist");
|
||||
}
|
||||
|
||||
const songs = data.items.map((item, index) => ({
|
||||
id: index + 1,
|
||||
youtubeId: item.contentDetails.videoId,
|
||||
title: item.snippet.title,
|
||||
artist: item.snippet.videoOwnerChannelTitle || "Unknown Artist",
|
||||
coverUrl: item.snippet.thumbnails.high?.url || item.snippet.thumbnails.default?.url ||
|
||||
"https://place-hold.it/500x500/333/fff?text=No%20Thumbnail",
|
||||
}));
|
||||
|
||||
cachedSongs = songs;
|
||||
lastFetchTime = now;
|
||||
console.log("Fetched and cached YouTube songs:", songs.length);
|
||||
|
||||
return songs;
|
||||
} catch (error) {
|
||||
console.error("Error fetching YouTube playlist:", error);
|
||||
|
||||
if (cachedSongs && cachedSongs.length > 0) {
|
||||
console.log("Using last cached songs due to API error");
|
||||
return cachedSongs;
|
||||
}
|
||||
|
||||
return [{
|
||||
id: 1,
|
||||
title: "Could not load songs",
|
||||
artist: "API Error",
|
||||
coverUrl: "https://place-hold.it/500x500/f00/fff?text=Error",
|
||||
youtubeId: "dQw4w9WgXcQ"
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
const getAvailableSongIds = async () => {
|
||||
const songs = await fetchPlaylistSongs();
|
||||
return songs.map(song => song.id);
|
||||
};
|
||||
|
||||
module.exports = {fetchPlaylistSongs, getAvailableSongIds};
|
Loading…
x
Reference in New Issue
Block a user