From dab720ef1bb79b6ac098c1beb3c879a644a452cb Mon Sep 17 00:00:00 2001 From: Lukas Weger Date: Sat, 2 Aug 2025 10:34:26 +0200 Subject: [PATCH 1/3] Add Docker configuration and .dockerignore for Serena application --- .dockerignore | 65 ++++++++++++++++++++++++ Dockerfile | 65 ++++++++++++++++++++++++ README.md | 124 ++++++++++++++++++++++++++++++++++++++++++++- docker-compose.yml | 69 +++++++++++++++++++++++++ 4 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..319ca6d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,65 @@ +# Git +.git +.gitignore + +# Documentation +README.md +LICENSE +*.md + +# Node modules (will be installed in container) +frontend/node_modules +node_modules + +# Build artifacts +backend/build +frontend/build +backend/.gradle +backend/bin + +# IDE files +.vscode +.idea +*.iml + +# OS files +.DS_Store +Thumbs.db + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity diff --git a/Dockerfile b/Dockerfile index e69de29..77ce287 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,65 @@ +# Multi-stage Dockerfile for Serena (Backend + Frontend) + +# Stage 1: Build Frontend +FROM node:18-alpine AS frontend-builder + +WORKDIR /app/frontend + +# Copy package files +COPY frontend/package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy frontend source +COPY frontend/ ./ + +# Build frontend +RUN npm run build + +# Stage 2: Build Backend +FROM openjdk:21-jdk-slim AS backend-builder + +WORKDIR /app/backend + +# Copy Gradle wrapper and build files +COPY backend/gradlew ./ +COPY backend/gradle/ ./gradle/ +COPY backend/build.gradle ./ +COPY backend/settings.gradle ./ + +# Copy source code +COPY backend/src/ ./src/ + +# Make gradlew executable and build +RUN chmod +x gradlew +RUN ./gradlew build --no-daemon -x test + +# Stage 3: Runtime +FROM openjdk:21-jre-slim + +WORKDIR /app + +# Install curl for health checks +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# Copy built backend JAR +COPY --from=backend-builder /app/backend/build/libs/*.jar app.jar + +# Copy built frontend (will be served by Spring Boot) +COPY --from=frontend-builder /app/frontend/build/ /app/static/ + +# Create non-root user +RUN groupadd -r serena && useradd -r -g serena serena +RUN chown -R serena:serena /app +USER serena + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/actuator/health || exit 1 + +# Run the application +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 759fb12..d07009c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,124 @@ -# team-5 +# Serena - Collaborative Radio Station Platform +**Serena** is a collaborative radio station platform that enables users to create and join virtual music stations for shared listening experiences. The application features a React frontend and Spring Boot backend, integrating with the Spotify API to provide real-time music playback and queue management. + +## ๐ŸŽต Features + +- **Create & Join Stations**: Users can create radio stations with unique join codes or join existing stations +- **Collaborative Queuing**: Multiple users can add songs to their preferred queues +- **Intelligent Recommendations**: Advanced algorithm considers song popularity, tempo similarity, audio features, and user preferences +- **Real-time Playback**: Spotify integration for seamless music streaming (station owners control playback) +- **User Authentication**: JWT-based authentication system with owner and client roles +- **Responsive UI**: Modern React interface with animated backgrounds and intuitive controls + +## ๐Ÿ—๏ธ Architecture + +### Backend (Spring Boot) +- **Language**: Java +- **Framework**: Spring Boot +- **Authentication**: JWT tokens +- **Queue Algorithm**: Multi-factor recommendation system +- **API**: RESTful endpoints for station and queue management + +### Frontend (React) +- **Language**: JavaScript (JSX) +- **Framework**: React 19.1.1 +- **Routing**: React Router DOM +- **Spotify Integration**: Web Playback SDK +- **Styling**: CSS with animations + +### External APIs +- **Spotify Web API**: Music data and playback control +- **Spotify Web Playback SDK**: Browser-based music streaming + +## ๐Ÿš€ Quick Start + +### Prerequisites +- Node.js (v16+) +- Java 11+ +- Gradle +- Docker (optional) +- Spotify Premium account (for playback control) + + +### Backend Setup +```bash +cd backend +./gradlew bootRun +``` + +The backend will start on `http://localhost:8080` + +### Frontend Setup +```bash +cd frontend +npm install +npm start +``` + +The frontend will start on `http://localhost:3000` + +### Docker Setup (Alternative) +```bash +docker-compose up +``` + +## ๐Ÿ“– API Documentation + +Detailed API documentation is available in [`backend/API_DOCUMENTATION.md`](backend/API_DOCUMENTATION.md). + +### Key Endpoints +- `POST /api/radio-stations` - Create a new radio station +- `GET /api/radio-stations` - List all radio stations +- `POST /api/clients/connect` - Join a station with join code +- `POST /api/songs/queue` - Add song to client's preferred queue +- `GET /api/songs/queue/recommended` - Get intelligently sorted queue + +## ๐ŸŽฏ How It Works + +### Station Creation +1. User creates a station with name and description +2. System generates unique station ID and 6-character join code +3. Creator becomes the station owner with playback control + +### Joining Stations +1. Users join with the station's join code +2. Receive client authentication token +3. Can add songs to their preferred queue + +### Queue Algorithm +The intelligent queue system considers: +- **Preferred Queue Position** (30%): User song preferences +- **Popularity** (20%): Spotify popularity scores +- **Tempo Similarity** (20%): BPM matching with current song +- **Audio Features** (30%): Danceability, energy, valence, etc. + +### Playback Control +- Only station owners can control playback +- Automatic progression through recommended queue +- Real-time sync with Spotify Web Playback SDK + +## ๐Ÿ”ง Configuration + +### Backend Configuration +Update `backend/src/main/resources/application.properties`: +```properties +# Add your Spotify credentials and other settings +``` + +### Frontend Configuration +Update Spotify credentials in `frontend/src/constants/ApiConstants.js` + +## ๐Ÿงช Testing + +### Backend Tests +```bash +cd backend +./gradlew test +``` + +### Frontend Tests +```bash +cd frontend +npm test +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e69de29..4624634 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -0,0 +1,69 @@ +version: '3.8' + +services: + # Backend Service + serena-backend: + build: + context: . + dockerfile: Dockerfile + container_name: serena-backend + ports: + - "8080:8080" + environment: + - SPRING_PROFILES_ACTIVE=docker + - SERVER_PORT=8080 + - SPRING_WEB_CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + restart: unless-stopped + networks: + - serena-network + + # Frontend Development Service (alternative to built-in static files) + serena-frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + container_name: serena-frontend + ports: + - "3000:3000" + environment: + - REACT_APP_API_URL=http://localhost:8080 + - CHOKIDAR_USEPOLLING=true + volumes: + - ./frontend:/app + - /app/node_modules + depends_on: + - serena-backend + networks: + - serena-network + profiles: + - development + + # Production Frontend (served by backend) + serena-app: + build: + context: . + dockerfile: Dockerfile + container_name: serena-app + ports: + - "80:8080" + environment: + - SPRING_PROFILES_ACTIVE=production + - SERVER_PORT=8080 + restart: unless-stopped + networks: + - serena-network + profiles: + - production + +networks: + serena-network: + driver: bridge + +volumes: + node_modules: \ No newline at end of file From 0f020d83d226d90cb7259dff0a8b2468f506c2fa Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 2 Aug 2025 10:39:04 +0200 Subject: [PATCH 2/3] Reduce --- frontend/src/components/LoginButton.jsx | 30 ------------------- frontend/src/constants/ApiConstants.js | 3 -- frontend/src/screens/StationPage.jsx | 17 ----------- frontend/src/utils/spotifyAuth.js | 40 ------------------------- 4 files changed, 90 deletions(-) delete mode 100644 frontend/src/components/LoginButton.jsx delete mode 100644 frontend/src/utils/spotifyAuth.js diff --git a/frontend/src/components/LoginButton.jsx b/frontend/src/components/LoginButton.jsx deleted file mode 100644 index f928e40..0000000 --- a/frontend/src/components/LoginButton.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { getSpotifyLoginUrl, isLoggedIn, removeAccessToken } from '../utils/spotifyAuth.js'; - -const LoginButton = () => { - const loggedIn = isLoggedIn(); - - const handleLogout = () => { - removeAccessToken(); - window.location.reload(); // Refresh to update UI - }; - - if (loggedIn) { - return ( -
- โœ“ Connected to Spotify - -
- ); - } - - return ( - - - - ); -}; - -export default LoginButton; diff --git a/frontend/src/constants/ApiConstants.js b/frontend/src/constants/ApiConstants.js index caaf274..7cdec44 100644 --- a/frontend/src/constants/ApiConstants.js +++ b/frontend/src/constants/ApiConstants.js @@ -3,6 +3,3 @@ export const RADIOSTATION_URL = "http://localhost:8080/api"; export const CREATE_RADIOSTATION_ENDPOINT = "/radio-stations"; export const LIST_RADIOSTATIONS_ENDPOINT = "/radio-stations"; export const ADD_CLIENT_ENDPOINT = "/clients/connect"; - -export const SPOTIFY_PREMIUM_TOKEN = - "BQCEnWrYVdmta4Eekdkewazun99IuocRAyPEbwPSrHuGYgZYg7JGlXG2BLmRL6fwjzPJkrmHiqlTLSn1yqR36awA9Rv4n9dwvTBv1DjAitsuzaEVg7PtYdbUHXqP2HJJ4dDDvTtvUfWIBDY_Afa7WgY1cyRbl-p4VobNHXUR9N3Ye1qBTgH3RZ5ziIbIoNWe_JrxYvcedkvr23zXUVabOyahTgt_YdmnCWt2Iu8XT8sjhSyc8tOCqbs_KqE-Qe1WSPUCrGS8"; diff --git a/frontend/src/screens/StationPage.jsx b/frontend/src/screens/StationPage.jsx index c7698a0..b3e5062 100644 --- a/frontend/src/screens/StationPage.jsx +++ b/frontend/src/screens/StationPage.jsx @@ -1,13 +1,10 @@ import { useParams } from "react-router-dom"; import React, { useState, useEffect } from "react"; -import AddSongModal from "./AddSongModal"; -import LoginButton from "../components/LoginButton"; import { addClientToStation } from "../utils/AddClientToStation"; function StationPage() { const { stationID } = useParams(); const [clientToken, setClientToken] = useState(""); - const [isModalOpen, setIsModalOpen] = useState(false); const [accessDenied, setAccessDenied] = useState(true); const [userName, setUserName] = useState(""); @@ -36,14 +33,6 @@ function StationPage() { const recommendedSongs = []; - const openModal = () => { - setIsModalOpen(true); - }; - - const closeModal = () => { - setIsModalOpen(false); - }; - return (
@@ -56,15 +45,11 @@ function StationPage() {

Serena Station

Collaborative Music Experience

-

Song Queue

-
@@ -86,8 +71,6 @@ function StationPage() {
- - {isModalOpen && }
); } diff --git a/frontend/src/utils/spotifyAuth.js b/frontend/src/utils/spotifyAuth.js deleted file mode 100644 index 67fc94a..0000000 --- a/frontend/src/utils/spotifyAuth.js +++ /dev/null @@ -1,40 +0,0 @@ -const CLIENT_ID = 'e1274b6593674771bea12d8366c7978b'; -const REDIRECT_URI = 'http://localhost:3000/callback'; -const SCOPES = [ - 'user-read-private', - 'user-read-email', - 'playlist-read-private', - 'user-library-read', -]; - -export const getSpotifyLoginUrl = () => { - const scope = SCOPES.join('%20'); - return `https://accounts.spotify.com/authorize?client_id=${CLIENT_ID}&response_type=token&redirect_uri=${encodeURIComponent( - REDIRECT_URI - )}&scope=${scope}`; -}; - -export const getAccessTokenFromUrl = () => { - const hash = window.location.hash; - if (hash) { - const params = new URLSearchParams(hash.substring(1)); - return params.get('access_token'); - } - return null; -}; - -export const storeAccessToken = (token) => { - localStorage.setItem('spotify_access_token', token); -}; - -export const getAccessToken = () => { - return localStorage.getItem('spotify_access_token'); -}; - -export const removeAccessToken = () => { - localStorage.removeItem('spotify_access_token'); -}; - -export const isLoggedIn = () => { - return !!getAccessToken(); -}; From 0904b0e2190423fd7193211497cedc75a8937043 Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 2 Aug 2025 11:11:51 +0200 Subject: [PATCH 3/3] Unga bunga --- frontend/.gitignore | 2 ++ frontend/src/components/AudioPlayer.jsx | 35 +++++++++++++++++++++++++ frontend/src/screens/StationPage.jsx | 15 ++++------- frontend/src/utils/AudioFiles.js | 5 ++++ 4 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/AudioPlayer.jsx create mode 100644 frontend/src/utils/AudioFiles.js diff --git a/frontend/.gitignore b/frontend/.gitignore index 4d29575..fe844a4 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +music/* \ No newline at end of file diff --git a/frontend/src/components/AudioPlayer.jsx b/frontend/src/components/AudioPlayer.jsx new file mode 100644 index 0000000..e8fea80 --- /dev/null +++ b/frontend/src/components/AudioPlayer.jsx @@ -0,0 +1,35 @@ +import React, { useRef, useState } from "react"; + +const SingleAudioPlayer = ({ src }) => { + const audioRef = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); + + const togglePlay = () => { + const audio = audioRef.current; + if (!audio) return; + + if (isPlaying) { + audio.pause(); + } else { + audio.play().catch((e) => { + console.warn("Playback error:", e); + }); + } + + setIsPlaying(!isPlaying); + }; + + const handleEnded = () => { + setIsPlaying(false); + }; + + return ( +
+ + {src.split("/").pop()} +
+ ); +}; + +export default SingleAudioPlayer; diff --git a/frontend/src/screens/StationPage.jsx b/frontend/src/screens/StationPage.jsx index b3e5062..50df4e3 100644 --- a/frontend/src/screens/StationPage.jsx +++ b/frontend/src/screens/StationPage.jsx @@ -1,6 +1,8 @@ import { useParams } from "react-router-dom"; import React, { useState, useEffect } from "react"; import { addClientToStation } from "../utils/AddClientToStation"; +import { getAudioFiles } from "../utils/AudioFiles"; +import SingleAudioPlayer from "../components/AudioPlayer"; function StationPage() { const { stationID } = useParams(); @@ -31,7 +33,7 @@ function StationPage() { ); } - const recommendedSongs = []; + const recommendedSongs = getAudioFiles() ?? []; return (
@@ -46,6 +48,7 @@ function StationPage() {

Serena Station

Collaborative Music Experience

+
@@ -58,15 +61,7 @@ function StationPage() {

No songs in queue yet. Add some songs to get started!

) : ( - recommendedSongs.map((song) => ( -
-
-

{song.title}

-

{song.artist}

-
- {song.duration} -
- )) + recommendedSongs.map((song) =>

{song}

) )}
diff --git a/frontend/src/utils/AudioFiles.js b/frontend/src/utils/AudioFiles.js new file mode 100644 index 0000000..36568f4 --- /dev/null +++ b/frontend/src/utils/AudioFiles.js @@ -0,0 +1,5 @@ +const importAll = (r) => r.keys().map(r); + +export const getAudioFiles = () => { + return importAll(require.context("../../music", false, /\.(mp3|wav)$/)); +};