From 9901c1a49e6745c5f73e0dccaff2de6dcba75558 Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Sat, 1 Mar 2025 16:56:11 +0100 Subject: [PATCH] Add playlists --- server/controller/room.js | 66 +++++++++---- server/handler/connection.js | 24 ++++- server/services/youtubeService.js | 154 ++++++++++++++++++++++++++---- 3 files changed, 203 insertions(+), 41 deletions(-) diff --git a/server/controller/room.js b/server/controller/room.js index f5fb176..f08547b 100644 --- a/server/controller/room.js +++ b/server/controller/room.js @@ -1,3 +1,5 @@ +const youtubeService = require('../services/youtubeService'); + let cleanupGameState; const setCleanupGameState = (cleanupFunction) => { @@ -10,25 +12,41 @@ module.exports.roomExists = (roomId) => rooms[roomId] !== undefined; module.exports.isRoomOpen = (roomId) => rooms[roomId] && rooms[roomId].state === 'waiting'; -const initializeRoom = (roomId, user) => { - rooms[roomId] = { - members: [{...user, creator: true}], - settings: {}, - state: 'waiting', - playlistVotes: {}, - selectedPlaylist: null - }; +const initializeRoom = async (roomId, user) => { + try { + const randomPlaylists = await youtubeService.getRandomPlaylists(3); + rooms[roomId] = { + members: [{...user, creator: true}], + settings: {}, + state: 'waiting', + playlistVotes: {}, + selectedPlaylist: null, + availablePlaylists: randomPlaylists + }; + } catch (error) { + console.error("Error initializing room playlists:", error); + rooms[roomId] = { + members: [{...user, creator: true}], + settings: {}, + state: 'waiting', + playlistVotes: {}, + selectedPlaylist: null, + availablePlaylists: { + seventies: youtubeService.PLAYLISTS.seventies + } + }; + } }; -module.exports.connectUserToRoom = (roomId, user) => { +module.exports.connectUserToRoom = async (roomId, user) => { roomId = roomId.toUpperCase(); if (rooms[roomId]) { rooms[roomId].members.push({...user, creator: false}); } else { - initializeRoom(roomId, user); + await initializeRoom(roomId, user); } console.log(`User ${user.name} connected to room ${roomId}`); -} +}; module.exports.getUserRoom = (userId) => { for (const roomId in rooms) { @@ -88,7 +106,6 @@ module.exports.disconnectUser = (userId) => { if (memberIndex !== -1) { if (room.members[memberIndex].creator && room.members.length > 1) { - // Transfer host status to the next user room.members[1].creator = true; } @@ -145,8 +162,7 @@ module.exports.voteForPlaylist = (roomId, userId, playlistId) => { const room = rooms[roomId]; if (room.state !== 'waiting') return false; - - // Remove previous vote if exists + const previousVote = Object.entries(room.playlistVotes) .find(([_, voters]) => voters.includes(userId)); @@ -154,8 +170,7 @@ module.exports.voteForPlaylist = (roomId, userId, playlistId) => { room.playlistVotes[previousVote[0]] = room.playlistVotes[previousVote[0]].filter(id => id !== userId); } - - // Add new vote + if (!room.playlistVotes[playlistId]) { room.playlistVotes[playlistId] = []; } @@ -172,7 +187,7 @@ module.exports.getWinningPlaylist = (roomId) => { const room = rooms[roomId]; if (!room || !room.playlistVotes) { console.log(`No votes found for room ${roomId}, using default playlist`); - return Object.values(require('../services/youtubeService').PLAYLISTS)[0]; + return room.availablePlaylists[Object.keys(room.availablePlaylists)[0]]; } let maxVotes = 0; @@ -190,10 +205,23 @@ module.exports.getWinningPlaylist = (roomId) => { }); if (!winningPlaylist) { - console.log('No winning playlist found, using default'); - winningPlaylist = Object.values(require('../services/youtubeService').PLAYLISTS)[0]; + console.log('No winning playlist found, using first available'); + winningPlaylist = room.availablePlaylists[Object.keys(room.availablePlaylists)[0]]; } console.log(`Selected winning playlist: ${winningPlaylist}`); return winningPlaylist; +}; + +module.exports.getAvailablePlaylists = (roomId) => { + return rooms[roomId]?.availablePlaylists || {}; +}; + +module.exports.updateAvailablePlaylists = (roomId, newPlaylists) => { + if (rooms[roomId]) { + rooms[roomId].availablePlaylists = newPlaylists; + rooms[roomId].playlistVotes = {}; + return true; + } + return false; }; \ No newline at end of file diff --git a/server/handler/connection.js b/server/handler/connection.js index bbcd82e..582ce81 100644 --- a/server/handler/connection.js +++ b/server/handler/connection.js @@ -320,11 +320,29 @@ module.exports = (io) => (socket) => { socket.on("get-playlist-options", async () => { try { - const playlists = await youtubeService.getPlaylistDetails(); - socket.emit("playlist-options", playlists); + const roomId = roomController.getUserRoom(socket.id); + if (!roomId) { + throw new Error("User not in a room"); + } + + const availablePlaylists = roomController.getAvailablePlaylists(roomId); + const details = await youtubeService.getPlaylistDetails(availablePlaylists); + + if (Object.keys(details).length === 0) { + const newPlaylists = await youtubeService.getRandomPlaylists(3); + const newDetails = await youtubeService.getPlaylistDetails(newPlaylists); + + roomController.updateAvailablePlaylists(roomId, newPlaylists); + socket.emit("playlist-options", newDetails); + } else { + socket.emit("playlist-options", details); + } } catch (error) { console.error("Error fetching playlist options:", error); - socket.emit("error", { message: "Failed to load playlists" }); + socket.emit("error", { + message: "Failed to load playlists", + details: error.message + }); } }); diff --git a/server/services/youtubeService.js b/server/services/youtubeService.js index f3087c3..360e76d 100644 --- a/server/services/youtubeService.js +++ b/server/services/youtubeService.js @@ -2,12 +2,65 @@ const { google } = require('googleapis'); const youtube = google.youtube('v3'); const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY; -const PLAYLIST_ID = "PLmXxqSJJq-yXrCPGIT2gn8b34JjOrl4Xf"; const PLAYLISTS = { seventies: 'PLmXxqSJJq-yXrCPGIT2gn8b34JjOrl4Xf', eighties: 'PLmXxqSJJq-yUvMWKuZQAB_8yxnjZaOZUp', - nineties: 'PLmXxqSJJq-yUF3jbzjF_pa--kuBuMlyQQ' + nineties: 'PLmXxqSJJq-yUF3jbzjF_pa--kuBuMlyQQ', + pop: 'PLxA687tYuMWhkqYjvAGtW_heiEL4Hk_Lx', + dance: 'PL64E6BD94546734D8' +}; + +let validatedPlaylists = {}; +const VALIDATION_TTL = 3600000; // 1 hour + +const validatePlaylist = async (playlistId) => { + try { + const response = await youtube.playlists.list({ + key: API_KEY, + part: 'snippet,contentDetails', + id: playlistId + }); + + if (!response.data.items || response.data.items.length === 0) { + console.log(`Playlist ${playlistId} not found or empty`); + return false; + } + + validatedPlaylists[playlistId] = { + timestamp: Date.now(), + valid: true + }; + return true; + } catch (error) { + console.error(`Failed to validate playlist ${playlistId}:`, error); + return false; + } +}; + +const validateAndCleanPlaylists = async () => { + const now = Date.now(); + const validPlaylistIds = []; + + for (const [genre, playlistId] of Object.entries(PLAYLISTS)) { + if (validatedPlaylists[playlistId] && + (now - validatedPlaylists[playlistId].timestamp) < VALIDATION_TTL) { + if (validatedPlaylists[playlistId].valid) { + validPlaylistIds.push([genre, playlistId]); + } + continue; + } + + const isValid = await validatePlaylist(playlistId); + if (isValid) { + validPlaylistIds.push([genre, playlistId]); + } else { + console.log(`Removing invalid playlist: ${genre} (${playlistId})`); + delete PLAYLISTS[genre]; + } + } + + return validPlaylistIds; }; const API_KEY = process.env.YOUTUBE_API_KEY; @@ -22,7 +75,7 @@ const CACHE_TTL = 3600000; // 1 hour const fetchPlaylistSongs = async (playlistId = null) => { if (!playlistId) { console.warn("No playlist ID provided, using default"); - playlistId = PLAYLISTS.seventies; // default fallback + playlistId = PLAYLISTS.eighties; } const now = Date.now(); @@ -74,28 +127,64 @@ const getAvailableSongIds = async (playlistId) => { return songs.map(song => song.id); }; -async function getPlaylistDetails() { +async function getPlaylistDetails(availablePlaylists = null) { try { + const playlistsToUse = availablePlaylists || await getRandomPlaylists(3); const details = {}; - - for (const [genre, playlistId] of Object.entries(PLAYLISTS)) { + const validationPromises = []; + + for (const [genre, playlistId] of Object.entries(playlistsToUse)) { + validationPromises.push( + youtube.playlists.list({ + key: API_KEY, + part: 'snippet,contentDetails', + id: playlistId + }).then(response => { + if (response.data.items?.[0]) { + const playlist = response.data.items[0]; + details[genre] = { + id: playlistId, + title: playlist.snippet.title, + description: playlist.snippet.description, + thumbnail: playlist.snippet.thumbnails.maxres || playlist.snippet.thumbnails.high, + songCount: playlist.contentDetails.itemCount, + votes: 0 + }; + } else { + console.warn(`Playlist not found: ${genre} (${playlistId})`); + } + }).catch(error => { + console.error(`Error fetching playlist ${playlistId}:`, error); + }) + ); + } + + await Promise.all(validationPromises); + + if (Object.keys(details).length === 0) { const response = await youtube.playlists.list({ key: API_KEY, part: 'snippet,contentDetails', - id: playlistId + id: PLAYLISTS.seventies }); - - const playlist = response.data.items[0]; - details[genre] = { - id: playlistId, - title: playlist.snippet.title, - description: playlist.snippet.description, - thumbnail: playlist.snippet.thumbnails.maxres || playlist.snippet.thumbnails.high, - songCount: playlist.contentDetails.itemCount, - votes: 0 - }; + + if (response.data.items?.[0]) { + const playlist = response.data.items[0]; + details.seventies = { + id: PLAYLISTS.seventies, + title: playlist.snippet.title, + description: playlist.snippet.description, + thumbnail: playlist.snippet.thumbnails.maxres || playlist.snippet.thumbnails.high, + songCount: playlist.contentDetails.itemCount, + votes: 0 + }; + } } - + + if (Object.keys(details).length === 0) { + throw new Error("No valid playlists found"); + } + return details; } catch (error) { console.error('Error fetching playlist details:', error); @@ -103,9 +192,36 @@ async function getPlaylistDetails() { } } +const getRandomPlaylists = async (count = 3) => { + try { + const allPlaylists = Object.entries(PLAYLISTS); + if (allPlaylists.length === 0) { + throw new Error("No playlists configured"); + } + + const shuffled = [...allPlaylists].sort(() => Math.random() - 0.5); + const selected = shuffled.slice(0, Math.min(count, allPlaylists.length)); + + const result = {}; + for (const [genre, playlistId] of selected) { + result[genre] = playlistId; + } + + return result; + } catch (error) { + console.error("Error getting random playlists:", error); + return { + seventies: PLAYLISTS.seventies + }; + } +}; + module.exports = { fetchPlaylistSongs, getAvailableSongIds, PLAYLISTS, - getPlaylistDetails + getPlaylistDetails, + getRandomPlaylists, + validateAndCleanPlaylists, + validatePlaylist };