From aa10b5b2ccf1fadaa1d10d9b7703e45d0dd4c344 Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Sat, 1 Mar 2025 16:11:01 +0100 Subject: [PATCH] Add custom playlists --- client/src/pages/WaitingRoom/WaitingRoom.jsx | 59 +++++- client/src/pages/WaitingRoom/styles.sass | 80 +++++++ package.json | 1 + pnpm-lock.yaml | 209 +++++++++++++++++++ server/controller/room.js | 44 +++- server/handler/connection.js | 20 ++ server/services/youtubeService.js | 47 ++++- 7 files changed, 450 insertions(+), 10 deletions(-) diff --git a/client/src/pages/WaitingRoom/WaitingRoom.jsx b/client/src/pages/WaitingRoom/WaitingRoom.jsx index 62cbc97..6aee2f5 100644 --- a/client/src/pages/WaitingRoom/WaitingRoom.jsx +++ b/client/src/pages/WaitingRoom/WaitingRoom.jsx @@ -15,14 +15,17 @@ export const WaitingRoom = () => { const [isHost, setIsHost] = useState(false); const [username, setUsername] = useState(""); const [copied, setCopied] = useState(false); + const [playlists, setPlaylists] = useState({}); + const [votes, setVotes] = useState({}); + const [selectedPlaylist, setSelectedPlaylist] = useState(null); const messageEndRef = useRef(null); useEffect(() => { - // Check if the user is a host and get other initial data send("check-host-status"); send("get-user-info"); send("get-room-users"); send("get-room-code"); + send("get-playlist-options"); const handleHostStatus = (status) => { setIsHost(status.isHost); @@ -72,6 +75,14 @@ export const WaitingRoom = () => { setCurrentState("Game"); }; + const handlePlaylistOptions = (options) => { + setPlaylists(options); + }; + + const handleVotesUpdated = (newVotes) => { + setVotes(newVotes); + }; + // Register event listeners const cleanupHostStatus = on("host-status", handleHostStatus); const cleanupUserInfo = on("user-info", handleUserInfo); @@ -82,8 +93,9 @@ export const WaitingRoom = () => { const cleanupUserDisconnected = on("user-disconnected", handleUserDisconnected); const cleanupChatMessage = on("chat-message", handleChatMessage); const cleanupGameStarted = on("game-started", handleGameStarted); + const cleanupPlaylistOptions = on("playlist-options", handlePlaylistOptions); + const cleanupVotesUpdated = on("playlist-votes-updated", handleVotesUpdated); - // Add welcome message setMessages([{ system: true, text: "Welcome to the waiting room! Waiting for others to join..." @@ -99,6 +111,8 @@ export const WaitingRoom = () => { cleanupUserDisconnected(); cleanupChatMessage(); cleanupGameStarted(); + cleanupPlaylistOptions(); + cleanupVotesUpdated(); }; }, [on, send, socket, setCurrentState]); @@ -125,7 +139,6 @@ export const WaitingRoom = () => { }; const handleLeaveRoom = () => { - // Disconnect from the current room if (socket) { socket.disconnect(); } @@ -138,7 +151,40 @@ export const WaitingRoom = () => { setTimeout(() => setCopied(false), 2000); }; - const minPlayersToStart = 1; // Set your minimum players requirement here + const handleVote = (playlistId) => { + setSelectedPlaylist(playlistId); + send("vote-playlist", { playlistId }); + }; + + const getVoteCount = (playlistId) => { + return votes[playlistId]?.length || 0; + }; + + const renderPlaylists = () => ( +
+ {Object.entries(playlists).map(([genre, playlist]) => ( +
handleVote(playlist.id)} + > +
+ {playlist.title} +
+ {getVoteCount(playlist.id)} + votes +
+
+
+

{genre.toUpperCase()}

+

{playlist.songCount} songs

+
+
+ ))} +
+ ); + + const minPlayersToStart = 1; const canStartGame = isHost && users.length >= minPlayersToStart; return ( @@ -227,6 +273,11 @@ export const WaitingRoom = () => { + +
+

Vote for Playlist

+ {renderPlaylists()} +
); diff --git a/client/src/pages/WaitingRoom/styles.sass b/client/src/pages/WaitingRoom/styles.sass index 3bae174..d963d2d 100644 --- a/client/src/pages/WaitingRoom/styles.sass +++ b/client/src/pages/WaitingRoom/styles.sass @@ -330,6 +330,86 @@ font-size: 1.4rem margin: 0 +.playlist-section + margin-bottom: 30px + + h2 + color: $white + text-align: center + margin-bottom: 20px + font-size: 24px + text-shadow: 0 0 10px rgba(255, 255, 255, 0.5) + +.playlists-grid + display: grid + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) + gap: 20px + padding: 20px + +.playlist-card + background: rgba(255, 255, 255, 0.1) + border-radius: 15px + overflow: hidden + transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) + cursor: pointer + border: 2px solid transparent + + &:hover + transform: translateY(-5px) + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3) + background: rgba(255, 255, 255, 0.15) + + &.selected + border-color: $yellow + box-shadow: 0 0 30px rgba($yellow, 0.3) + + .playlist-thumbnail + position: relative + width: 100% + padding-top: 56.25% + + img + position: absolute + top: 0 + left: 0 + width: 100% + height: 100% + object-fit: cover + + .vote-count + position: absolute + top: 10px + right: 10px + background: rgba(0, 0, 0, 0.8) + padding: 8px 12px + border-radius: 20px + display: flex + flex-direction: column + align-items: center + + span + color: $white + + &:first-child + font-size: 24px + font-weight: bold + + &.vote-label + font-size: 12px + opacity: 0.8 + + .playlist-info + padding: 15px + + h3 + color: $white + margin-bottom: 5px + font-size: 18px + + p + color: rgba(255, 255, 255, 0.7) + font-size: 14px + @keyframes pop 0% transform: scale(1) diff --git a/package.json b/package.json index 7016956..04bb4be 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "express": "^4.21.2", + "googleapis": "^146.0.0", "socket.io": "^4.8.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cd3c7d..6d7fec9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: express: specifier: ^4.21.2 version: 4.21.2 + googleapis: + specifier: ^146.0.0 + version: 146.0.0 socket.io: specifier: ^4.8.1 version: 4.8.1 @@ -37,6 +40,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -55,10 +62,16 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64id@2.0.0: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -74,6 +87,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -165,6 +181,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -214,6 +233,9 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -238,6 +260,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -254,10 +284,30 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + googleapis-common@7.2.0: + resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} + engines: {node: '>=14.0.0'} + + googleapis@146.0.0: + resolution: {integrity: sha512-NewqvhnBZOJsugCAOo636O0BGE/xY7Cg/v8Rjm1+5LkJCjcqAzLleJ6igd5vrRExJLSKrY9uHy9iKE7r0PrfhQ==} + engines: {node: '>=14.0.0'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -278,6 +328,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -312,6 +366,19 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -356,6 +423,15 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + nodemon@3.1.9: resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==} engines: {node: '>=10'} @@ -511,6 +587,9 @@ packages: resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} hasBin: true + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -532,14 +611,27 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -585,6 +677,8 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + agent-base@7.1.3: {} + ansi-regex@5.0.1: {} ansi-styles@4.3.0: @@ -600,8 +694,12 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: {} + base64id@2.0.0: {} + bignumber.js@9.1.2: {} + binary-extensions@2.3.0: {} body-parser@1.20.3: @@ -630,6 +728,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-equal-constant-time@1.0.1: {} + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -720,6 +820,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} emoji-regex@8.0.0: {} @@ -796,6 +900,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -821,6 +927,26 @@ snapshots: function-bind@1.1.2: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + get-caller-file@2.0.5: {} get-intrinsic@1.3.0: @@ -845,8 +971,50 @@ snapshots: dependencies: is-glob: 4.0.3 + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + + googleapis-common@7.2.0: + dependencies: + extend: 3.0.2 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + qs: 6.13.0 + url-template: 2.0.8 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + googleapis@146.0.0: + dependencies: + google-auth-library: 9.15.1 + googleapis-common: 7.2.0 + transitivePeerDependencies: + - encoding + - supports-color + gopd@1.2.0: {} + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -865,6 +1033,13 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.3.7(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -889,6 +1064,23 @@ snapshots: is-number@7.0.0: {} + is-stream@2.0.1: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.1.2 + + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + lodash@4.17.21: {} math-intrinsics@1.1.0: {} @@ -917,6 +1109,10 @@ snapshots: negotiator@0.6.3: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + nodemon@3.1.9: dependencies: chokidar: 3.6.0 @@ -1107,6 +1303,8 @@ snapshots: touch@3.1.1: {} + tr46@0.0.3: {} + tree-kill@1.2.2: {} tslib@2.8.1: {} @@ -1122,10 +1320,21 @@ snapshots: unpipe@1.0.0: {} + url-template@2.0.8: {} + utils-merge@1.0.1: {} + uuid@9.0.1: {} + vary@1.1.2: {} + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/server/controller/room.js b/server/controller/room.js index 2c46cee..ce7876c 100644 --- a/server/controller/room.js +++ b/server/controller/room.js @@ -10,16 +10,22 @@ 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 + }; +}; + module.exports.connectUserToRoom = (roomId, user) => { roomId = roomId.toUpperCase(); if (rooms[roomId]) { rooms[roomId].members.push({...user, creator: false}); } else { - rooms[roomId] = { - members: [{...user, creator: true}], - settings: {}, - state: 'waiting' - }; + initializeRoom(roomId, user); } console.log(`User ${user.name} connected to room ${roomId}`); } @@ -132,4 +138,32 @@ module.exports.getUserName = (userId) => { } } return null; +}; + +module.exports.voteForPlaylist = (roomId, userId, playlistId) => { + if (!rooms[roomId]) return false; + + 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)); + + if (previousVote) { + room.playlistVotes[previousVote[0]] = + room.playlistVotes[previousVote[0]].filter(id => id !== userId); + } + + // Add new vote + if (!room.playlistVotes[playlistId]) { + room.playlistVotes[playlistId] = []; + } + room.playlistVotes[playlistId].push(userId); + + return true; +}; + +module.exports.getPlaylistVotes = (roomId) => { + return rooms[roomId]?.playlistVotes || {}; }; \ No newline at end of file diff --git a/server/handler/connection.js b/server/handler/connection.js index 824c202..2d47f1e 100644 --- a/server/handler/connection.js +++ b/server/handler/connection.js @@ -308,4 +308,24 @@ module.exports = (io) => (socket) => { socket.emit("playlist-songs", { songs: youtubeService.getDefaultSongs() }); } }); + + socket.on("get-playlist-options", async () => { + try { + const playlists = await youtubeService.getPlaylistDetails(); + socket.emit("playlist-options", playlists); + } catch (error) { + console.error("Error fetching playlist options:", error); + socket.emit("error", { message: "Failed to load playlists" }); + } + }); + + socket.on("vote-playlist", ({ playlistId }) => { + const roomId = roomController.getUserRoom(socket.id); + if (!roomId) return; + + if (roomController.voteForPlaylist(roomId, socket.id, playlistId)) { + const votes = roomController.getPlaylistVotes(roomId); + io.to(roomId).emit("playlist-votes-updated", votes); + } + }); }; \ No newline at end of file diff --git a/server/services/youtubeService.js b/server/services/youtubeService.js index 11887c6..8d8770d 100644 --- a/server/services/youtubeService.js +++ b/server/services/youtubeService.js @@ -1,6 +1,17 @@ +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' +}; + +const API_KEY = process.env.YOUTUBE_API_KEY; + let cachedSongs = null; let lastFetchTime = 0; const CACHE_TTL = 3600000; // 1 hour @@ -68,4 +79,38 @@ const getAvailableSongIds = async () => { return songs.map(song => song.id); }; -module.exports = {fetchPlaylistSongs, getAvailableSongIds}; +async function getPlaylistDetails() { + try { + const details = {}; + + for (const [genre, playlistId] of Object.entries(PLAYLISTS)) { + const response = await youtube.playlists.list({ + key: API_KEY, + part: 'snippet,contentDetails', + id: playlistId + }); + + 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 + }; + } + + return details; + } catch (error) { + console.error('Error fetching playlist details:', error); + throw error; + } +} + +module.exports = { + fetchPlaylistSongs, + getAvailableSongIds, + PLAYLISTS, + getPlaylistDetails +};