From 98baaed4846a7d6feb888e36f97eec49a5ca3c6d Mon Sep 17 00:00:00 2001 From: Lukas Weger Date: Sat, 2 Aug 2025 03:30:48 +0200 Subject: [PATCH] Implement song queue management for clients, including adding songs to preferred queue and retrieving the queue for a radio station --- backend/API_DOCUMENTATION.md | 172 ++++++++++++++++++ .../backend/controller/SongController.java | 36 +++- .../serena/backend/dto/AddSongRequest.java | 3 +- .../dto/AddSongToClientQueueRequest.java | 41 +++++ .../java/com/serena/backend/model/Client.java | 20 ++ .../serena/backend/model/RadioStation.java | 6 +- .../backend/service/RadioStationService.java | 34 +++- 7 files changed, 289 insertions(+), 23 deletions(-) create mode 100644 backend/src/main/java/com/serena/backend/dto/AddSongToClientQueueRequest.java diff --git a/backend/API_DOCUMENTATION.md b/backend/API_DOCUMENTATION.md index 7f5482d..690ee97 100644 --- a/backend/API_DOCUMENTATION.md +++ b/backend/API_DOCUMENTATION.md @@ -23,6 +23,26 @@ Authorization: Bearer #### Create Radio Station +#### Get All Radio Stations + +#### Get Radio Station by ID + +#### Delete Radio Station + +### Client Management + +#### Connect Client to Station + +### Song Queue Management + +#### Add Song to Client's Preferred Queue + +#### Get Radio Station Queue + +## Radio Station Management + +#### Create Radio Station + Creates a new radio station and returns an owner token. **Endpoint:** `POST /radio-stations` @@ -170,6 +190,90 @@ Connects a client to a radio station using a join code. No authentication requir } ``` +### Song Queue Management + +#### Add Song to Client's Preferred Queue + +Adds a song to a specific client's preferred queue. The frontend can merge track and audio features objects using `const merged = { ...trackObj, ...audioFeaturesObj };` + +**Endpoint:** `POST /api/songs/queue` + +**Request Body:** + +```json +{ + "song": { + "id": "4iV5W9uYEdYUVa79Axb7Rh", + "popularity": 85, + "tempo": 120.5, + "danceability": 0.8, + "energy": 0.7, + "valence": 0.6, + "acousticness": 0.1, + "instrumentalness": 0.0, + "liveness": 0.1, + "speechiness": 0.04 + }, + "clientId": "client-uuid", + "radioStationId": "station-uuid" +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Song added to client's preferred queue successfully", + "data": null +} +``` + +#### Get Radio Station Queue + +Retrieves the main song queue for a radio station. Returns a list of song objects. + +**Endpoint:** `GET /api/songs/queue?radioStationId={radioStationId}` + +**Query Parameters:** + +- `radioStationId` (required): The ID of the radio station + +**Response:** + +```json +{ + "success": true, + "message": "Queue retrieved successfully", + "data": [ + { + "id": "4iV5W9uYEdYUVa79Axb7Rh", + "popularity": 85, + "tempo": 120.5, + "danceability": 0.8, + "energy": 0.7, + "valence": 0.6, + "acousticness": 0.1, + "instrumentalness": 0.0, + "liveness": 0.1, + "speechiness": 0.04 + }, + { + "id": "7ouMYWpwJ422jRcDASZB7P", + "popularity": 78, + "tempo": 95.0, + "danceability": 0.6, + "energy": 0.5, + "valence": 0.8, + "acousticness": 0.3, + "instrumentalness": 0.0, + "liveness": 0.2, + "speechiness": 0.06 + } + ] +} +``` + ## Error Responses All error responses follow this format: @@ -213,3 +317,71 @@ curl -X GET "http://localhost:8080/api/radio-stations" \ curl -X DELETE http://localhost:8080/api/radio-stations/{stationId} \ -H "Authorization: Bearer {ownerToken}" ``` + +### Adding a Song to Client's Preferred Queue + +```bash +curl -X POST http://localhost:8080/api/songs/queue \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {clientToken}" \ + -d '{ + "song": { + "id": "4iV5W9uYEdYUVa79Axb7Rh", + "popularity": 85, + "tempo": 120.5, + "danceability": 0.8, + "energy": 0.7, + "valence": 0.6, + "acousticness": 0.1, + "instrumentalness": 0.0, + "liveness": 0.1, + "speechiness": 0.04 + }, + "clientId": "client-uuid", + "radioStationId": "station-uuid" + }' +``` + +### Getting Radio Station Queue + +```bash +curl -X GET "http://localhost:8080/api/songs/queue?radioStationId=station-uuid" \ + -H "Authorization: Bearer {token}" +``` + +## Frontend Integration Notes + +### Merging Track and Audio Features + +The frontend can easily merge track information with audio features using JavaScript object spread: + +```javascript +// Track object from Spotify API +const trackObj = { + id: "4iV5W9uYEdYUVa79Axb7Rh", + popularity: 85, + tempo: 120.5, +}; + +// Audio features object from Spotify API +const audioFeaturesObj = { + danceability: 0.8, + energy: 0.7, + valence: 0.6, + acousticness: 0.1, + instrumentalness: 0.0, + liveness: 0.1, + speechiness: 0.04, +}; + +// Merge objects for API request +const merged = { ...trackObj, ...audioFeaturesObj }; +``` + +### Queue Data Structure + +The queue endpoints return/accept arrays of song objects. Each song object contains: + +- Basic track info (id, popularity, tempo) +- Audio features (danceability, energy, valence, etc.) +- All properties are at the root level (flattened structure) diff --git a/backend/src/main/java/com/serena/backend/controller/SongController.java b/backend/src/main/java/com/serena/backend/controller/SongController.java index 1694a5c..403784b 100644 --- a/backend/src/main/java/com/serena/backend/controller/SongController.java +++ b/backend/src/main/java/com/serena/backend/controller/SongController.java @@ -1,8 +1,8 @@ package com.serena.backend.controller; import com.serena.backend.dto.ApiResponse; -import com.serena.backend.dto.ConnectClientRequest; -import com.serena.backend.dto.AddSongRequest; +import com.serena.backend.dto.AddSongToClientQueueRequest; +import com.serena.backend.model.Song; import com.serena.backend.service.RadioStationService; import com.serena.backend.service.JwtService; import org.springframework.beans.factory.annotation.Autowired; @@ -10,8 +10,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import java.util.Optional; +import java.util.Queue; + @RestController -@RequestMapping("/api/songs/") +@RequestMapping("/api/songs") @CrossOrigin(origins = "*") public class SongController { @@ -21,20 +24,33 @@ public class SongController { @Autowired private JwtService jwtService; - @PostMapping - public ResponseEntity> addSong(@RequestBody AddSongRequest request) { - if (request.getSong() == null || request.getRadioStationId() == null) { + @PostMapping("/queue") + public ResponseEntity> addSongToClientQueue(@RequestBody AddSongToClientQueueRequest request) { + if (request.getSong() == null || request.getClientId() == null) { return ResponseEntity.badRequest() - .body(new ApiResponse<>(false, "Song data and radio station ID are required", null)); + .body(new ApiResponse<>(false, "Song data and client ID are required", null)); } - boolean success = radioStationService.addSongToQueue(request.getRadioStationId(), request.getSong()); + boolean success = radioStationService.addSongToClientQueue(request.getClientId(), request.getSong()); if (success) { - return ResponseEntity.ok(new ApiResponse<>(true, "Song added to queue successfully", null)); + return ResponseEntity.ok(new ApiResponse<>(true, "Song added to client's preferred queue successfully", null)); } else { return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(new ApiResponse<>(false, "Radio station not found or inactive", null)); + .body(new ApiResponse<>(false, "Client not found", null)); + } + } + + @GetMapping("/queue") + public ResponseEntity>> getRadioStationQueue(@RequestParam String radioStationId) { + Optional station = radioStationService.getRadioStation(radioStationId); + + if (station.isPresent()) { + Queue queue = station.get().getSongQueue(); + return ResponseEntity.ok(new ApiResponse<>(true, "Queue retrieved successfully", queue)); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ApiResponse<>(false, "Radio station not found", null)); } } } diff --git a/backend/src/main/java/com/serena/backend/dto/AddSongRequest.java b/backend/src/main/java/com/serena/backend/dto/AddSongRequest.java index be69f9f..ce6ed44 100644 --- a/backend/src/main/java/com/serena/backend/dto/AddSongRequest.java +++ b/backend/src/main/java/com/serena/backend/dto/AddSongRequest.java @@ -6,7 +6,8 @@ public class AddSongRequest { private Song song; private String radioStationId; - public AddSongRequest() {} + public AddSongRequest() { + } public AddSongRequest(Song song, String radioStationId) { this.song = song; diff --git a/backend/src/main/java/com/serena/backend/dto/AddSongToClientQueueRequest.java b/backend/src/main/java/com/serena/backend/dto/AddSongToClientQueueRequest.java new file mode 100644 index 0000000..05626db --- /dev/null +++ b/backend/src/main/java/com/serena/backend/dto/AddSongToClientQueueRequest.java @@ -0,0 +1,41 @@ +package com.serena.backend.dto; + +import com.serena.backend.model.Song; + +public class AddSongToClientQueueRequest { + private Song song; + private String clientId; + private String radioStationId; + + public AddSongToClientQueueRequest() {} + + public AddSongToClientQueueRequest(Song song, String clientId, String radioStationId) { + this.song = song; + this.clientId = clientId; + this.radioStationId = radioStationId; + } + + public Song getSong() { + return song; + } + + public void setSong(Song song) { + this.song = song; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getRadioStationId() { + return radioStationId; + } + + public void setRadioStationId(String radioStationId) { + this.radioStationId = radioStationId; + } +} diff --git a/backend/src/main/java/com/serena/backend/model/Client.java b/backend/src/main/java/com/serena/backend/model/Client.java index 4b5e2dd..b734aeb 100644 --- a/backend/src/main/java/com/serena/backend/model/Client.java +++ b/backend/src/main/java/com/serena/backend/model/Client.java @@ -2,16 +2,20 @@ package com.serena.backend.model; import java.time.LocalDateTime; import java.util.UUID; +import java.util.LinkedList; +import java.util.Queue; public class Client { private String id; private String username; private String radioStationId; private LocalDateTime connectedAt; + private Queue preferredQueue; public Client() { this.id = UUID.randomUUID().toString(); this.connectedAt = LocalDateTime.now(); + this.preferredQueue = new LinkedList<>(); } public Client(String username, String radioStationId) { @@ -53,4 +57,20 @@ public class Client { this.connectedAt = connectedAt; } + public Queue getPreferredQueue() { + return preferredQueue; + } + + public void setPreferredQueue(Queue preferredQueue) { + this.preferredQueue = preferredQueue; + } + + public void addSongToPreferredQueue(Song song) { + this.preferredQueue.offer(song); + } + + public Song getNextPreferredSong() { + return this.preferredQueue.poll(); + } + } diff --git a/backend/src/main/java/com/serena/backend/model/RadioStation.java b/backend/src/main/java/com/serena/backend/model/RadioStation.java index 693f480..27fb3bc 100644 --- a/backend/src/main/java/com/serena/backend/model/RadioStation.java +++ b/backend/src/main/java/com/serena/backend/model/RadioStation.java @@ -15,7 +15,7 @@ public class RadioStation { private String ownerId; private String joinCode; private LocalDateTime createdAt; - private List connectedClients; + private List connectedClients; private Queue songQueue; public RadioStation() { @@ -93,11 +93,11 @@ public class RadioStation { this.createdAt = createdAt; } - public List getConnectedClients() { + public List getConnectedClients() { return connectedClients; } - public void setConnectedClients(List connectedClients) { + public void setConnectedClients(List connectedClients) { this.connectedClients = connectedClients; } diff --git a/backend/src/main/java/com/serena/backend/service/RadioStationService.java b/backend/src/main/java/com/serena/backend/service/RadioStationService.java index 9ab557b..8af4122 100644 --- a/backend/src/main/java/com/serena/backend/service/RadioStationService.java +++ b/backend/src/main/java/com/serena/backend/service/RadioStationService.java @@ -54,9 +54,10 @@ public class RadioStationService { RadioStation station = radioStations.get(stationId); if (station != null) { // Disconnect all clients + for (Client client : station.getConnectedClients()) { + clients.remove(client.getId()); + } station.getConnectedClients().clear(); - // Remove from clients map - clients.entrySet().removeIf(entry -> stationId.equals(entry.getValue().getRadioStationId())); radioStations.remove(stationId); return true; } @@ -69,7 +70,7 @@ public class RadioStationService { if (station != null && station.getJoinCode().equals(joinCode)) { Client client = new Client(username, radioStationId); clients.put(client.getId(), client); - station.getConnectedClients().add(client.getId()); + station.getConnectedClients().add(client); return Optional.of(client); } return Optional.empty(); @@ -81,7 +82,7 @@ public class RadioStationService { RadioStation station = stationOpt.get(); Client client = new Client(username, station.getId()); clients.put(client.getId(), client); - station.getConnectedClients().add(client.getId()); + station.getConnectedClients().add(client); return Optional.of(client); } return Optional.empty(); @@ -92,7 +93,7 @@ public class RadioStationService { if (client != null) { RadioStation station = radioStations.get(client.getRadioStationId()); if (station != null) { - station.getConnectedClients().remove(clientId); + station.getConnectedClients().removeIf(c -> c.getId().equals(clientId)); } clients.remove(clientId); return true; @@ -107,10 +108,7 @@ public class RadioStationService { public List getConnectedClients(String radioStationId) { RadioStation station = radioStations.get(radioStationId); if (station != null) { - return station.getConnectedClients().stream() - .map(clients::get) - .filter(client -> client != null) - .toList(); + return new ArrayList<>(station.getConnectedClients()); } return new ArrayList<>(); } @@ -132,4 +130,22 @@ public class RadioStationService { return Optional.empty(); } + // Client Queue Management + public boolean addSongToClientQueue(String clientId, Song song) { + Client client = clients.get(clientId); + if (client != null) { + client.addSongToPreferredQueue(song); + return true; + } + return false; + } + + public Optional getNextClientSong(String clientId) { + Client client = clients.get(clientId); + if (client != null) { + return Optional.ofNullable(client.getNextPreferredSong()); + } + return Optional.empty(); + } + }