Merge remote-tracking branch 'origin/main' into tobi-dev
This commit is contained in:
commit
429111c49f
14 changed files with 280 additions and 673 deletions
|
@ -50,10 +50,7 @@ Creates a new radio station and returns an owner token.
|
|||
"ownerId": "owner-uuid",
|
||||
"joinCode": "ABC123",
|
||||
"createdAt": "2025-08-01T10:00:00",
|
||||
"active": true,
|
||||
"connectedClients": [],
|
||||
"songQueue": [],
|
||||
"currentlyPlaying": null
|
||||
"connectedClients": []
|
||||
},
|
||||
"ownerToken": "jwt-token-here",
|
||||
"message": "Radio station created successfully. Use this token to manage your station."
|
||||
|
@ -65,11 +62,7 @@ Creates a new radio station and returns an owner token.
|
|||
|
||||
Retrieves all radio stations. Requires authentication.
|
||||
|
||||
**Endpoint:** `GET /radio-stations?activeOnly=true`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `activeOnly` (boolean, default: false): Filter to only active stations
|
||||
**Endpoint:** `GET /radio-stations`
|
||||
|
||||
**Response:**
|
||||
|
||||
|
@ -85,10 +78,7 @@ Retrieves all radio stations. Requires authentication.
|
|||
"ownerId": "owner-uuid",
|
||||
"joinCode": "ABC123",
|
||||
"createdAt": "2025-08-01T10:00:00",
|
||||
"active": true,
|
||||
"connectedClients": ["client1", "client2"],
|
||||
"songQueue": [],
|
||||
"currentlyPlaying": null
|
||||
"connectedClients": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -113,73 +103,7 @@ Retrieves a specific radio station. Requires authentication.
|
|||
"ownerId": "owner-uuid",
|
||||
"joinCode": "ABC123",
|
||||
"createdAt": "2025-08-01T10:00:00",
|
||||
"active": true,
|
||||
"connectedClients": [],
|
||||
"songQueue": [],
|
||||
"currentlyPlaying": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Radio Station by Join Code
|
||||
|
||||
Retrieves a radio station using its join code. No authentication required.
|
||||
|
||||
**Endpoint:** `GET /radio-stations/join/{joinCode}`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"id": "station-uuid",
|
||||
"name": "Station Name",
|
||||
"description": "Station Description",
|
||||
"ownerId": "owner-uuid",
|
||||
"joinCode": "ABC123",
|
||||
"createdAt": "2025-08-01T10:00:00",
|
||||
"active": true,
|
||||
"connectedClients": [],
|
||||
"songQueue": [],
|
||||
"currentlyPlaying": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Update Radio Station
|
||||
|
||||
Updates a radio station. Only the station owner can update it.
|
||||
|
||||
**Endpoint:** `PUT /radio-stations/{stationId}`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Updated Station Name",
|
||||
"description": "Updated description"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Radio station updated successfully",
|
||||
"data": {
|
||||
"id": "station-uuid",
|
||||
"name": "Updated Station Name",
|
||||
"description": "Updated description",
|
||||
"ownerId": "owner-uuid",
|
||||
"joinCode": "ABC123",
|
||||
"createdAt": "2025-08-01T10:00:00",
|
||||
"active": true,
|
||||
"connectedClients": [],
|
||||
"songQueue": [],
|
||||
"currentlyPlaying": null
|
||||
"connectedClients": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -238,8 +162,7 @@ Connects a client to a radio station using a join code. No authentication requir
|
|||
"id": "client-uuid",
|
||||
"username": "john_doe",
|
||||
"radioStationId": "station-uuid",
|
||||
"connectedAt": "2025-08-01T10:00:00",
|
||||
"active": true
|
||||
"connectedAt": "2025-08-01T10:00:00"
|
||||
},
|
||||
"clientToken": "jwt-token-here",
|
||||
"message": "Successfully connected to radio station. Use this token for further requests."
|
||||
|
@ -247,258 +170,6 @@ Connects a client to a radio station using a join code. No authentication requir
|
|||
}
|
||||
```
|
||||
|
||||
#### Disconnect Client
|
||||
|
||||
Disconnects a client from a station. Only the station owner can disconnect clients.
|
||||
|
||||
**Endpoint:** `DELETE /clients/{clientId}/disconnect`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Client disconnected successfully",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Client Information
|
||||
|
||||
Retrieves information about a specific client. Requires authentication.
|
||||
|
||||
**Endpoint:** `GET /clients/{clientId}`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"id": "client-uuid",
|
||||
"username": "john_doe",
|
||||
"radioStationId": "station-uuid",
|
||||
"connectedAt": "2025-08-01T10:00:00",
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Connected Clients
|
||||
|
||||
Retrieves all clients connected to a radio station. Requires authentication.
|
||||
|
||||
**Endpoint:** `GET /clients/station/{radioStationId}`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Success",
|
||||
"data": [
|
||||
{
|
||||
"id": "client-uuid",
|
||||
"username": "john_doe",
|
||||
"radioStationId": "station-uuid",
|
||||
"connectedAt": "2025-08-01T10:00:00",
|
||||
"active": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Song Management
|
||||
|
||||
#### Add Song to Queue
|
||||
|
||||
Adds a song to the station's queue. Requires authentication.
|
||||
|
||||
**Endpoint:** `POST /radio-stations/{stationId}/songs`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Bohemian Rhapsody",
|
||||
"artist": "Queen",
|
||||
"album": "A Night at the Opera",
|
||||
"duration": 355,
|
||||
"url": "https://example.com/song.mp3"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Song added to queue successfully",
|
||||
"data": {
|
||||
"id": "song-uuid",
|
||||
"title": "Bohemian Rhapsody",
|
||||
"artist": "Queen",
|
||||
"album": "A Night at the Opera",
|
||||
"duration": 355,
|
||||
"url": "https://example.com/song.mp3",
|
||||
"addedBy": "user-uuid",
|
||||
"addedAt": "2025-08-01T10:00:00",
|
||||
"votes": {},
|
||||
"upvotes": 0,
|
||||
"downvotes": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Song Queue
|
||||
|
||||
Retrieves the current song queue, sorted by score (upvotes - downvotes). Requires authentication.
|
||||
|
||||
**Endpoint:** `GET /radio-stations/{stationId}/songs/queue`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Success",
|
||||
"data": [
|
||||
{
|
||||
"id": "song-uuid",
|
||||
"title": "Bohemian Rhapsody",
|
||||
"artist": "Queen",
|
||||
"album": "A Night at the Opera",
|
||||
"duration": 355,
|
||||
"url": "https://example.com/song.mp3",
|
||||
"addedBy": "user-uuid",
|
||||
"addedAt": "2025-08-01T10:00:00",
|
||||
"votes": {
|
||||
"user1": "UPVOTE",
|
||||
"user2": "DOWNVOTE"
|
||||
},
|
||||
"upvotes": 1,
|
||||
"downvotes": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Currently Playing Song
|
||||
|
||||
Retrieves the currently playing song. Requires authentication.
|
||||
|
||||
**Endpoint:** `GET /radio-stations/{stationId}/songs/current`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"id": "song-uuid",
|
||||
"title": "Bohemian Rhapsody",
|
||||
"artist": "Queen",
|
||||
"album": "A Night at the Opera",
|
||||
"duration": 355,
|
||||
"url": "https://example.com/song.mp3",
|
||||
"addedBy": "user-uuid",
|
||||
"addedAt": "2025-08-01T10:00:00",
|
||||
"votes": {},
|
||||
"upvotes": 0,
|
||||
"downvotes": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Play Next Song
|
||||
|
||||
Plays the next song from the queue (highest scoring song). Only station owners can control playback.
|
||||
|
||||
**Endpoint:** `POST /radio-stations/{stationId}/songs/next`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Playing next song",
|
||||
"data": {
|
||||
"id": "song-uuid",
|
||||
"title": "Bohemian Rhapsody",
|
||||
"artist": "Queen",
|
||||
"album": "A Night at the Opera",
|
||||
"duration": 355,
|
||||
"url": "https://example.com/song.mp3",
|
||||
"addedBy": "user-uuid",
|
||||
"addedAt": "2025-08-01T10:00:00",
|
||||
"votes": {},
|
||||
"upvotes": 0,
|
||||
"downvotes": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Vote on Song
|
||||
|
||||
Casts a vote (upvote or downvote) on a song. Requires authentication.
|
||||
|
||||
**Endpoint:** `POST /radio-stations/{stationId}/songs/{songId}/vote`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"voteType": "UPVOTE"
|
||||
}
|
||||
```
|
||||
|
||||
**Vote Types:**
|
||||
|
||||
- `UPVOTE`: Positive vote
|
||||
- `DOWNVOTE`: Negative vote
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Vote recorded successfully",
|
||||
"data": {
|
||||
"id": "song-uuid",
|
||||
"title": "Bohemian Rhapsody",
|
||||
"artist": "Queen",
|
||||
"album": "A Night at the Opera",
|
||||
"duration": 355,
|
||||
"url": "https://example.com/song.mp3",
|
||||
"addedBy": "user-uuid",
|
||||
"addedAt": "2025-08-01T10:00:00",
|
||||
"votes": {
|
||||
"current-user-id": "UPVOTE"
|
||||
},
|
||||
"upvotes": 1,
|
||||
"downvotes": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Remove Vote from Song
|
||||
|
||||
Removes the current user's vote from a song. Requires authentication.
|
||||
|
||||
**Endpoint:** `DELETE /radio-stations/{stationId}/songs/{songId}/vote`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Vote removed successfully",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
All error responses follow this format:
|
||||
|
@ -511,28 +182,9 @@ All error responses follow this format:
|
|||
}
|
||||
```
|
||||
|
||||
### Common HTTP Status Codes
|
||||
|
||||
- `200 OK`: Request successful
|
||||
- `201 Created`: Resource created successfully
|
||||
- `400 Bad Request`: Invalid request data
|
||||
- `401 Unauthorized`: Authentication required or token invalid
|
||||
- `403 Forbidden`: Access denied (e.g., not station owner)
|
||||
- `404 Not Found`: Resource not found
|
||||
|
||||
### Common Error Messages
|
||||
|
||||
- `"User not authenticated"`: Missing or invalid authentication token
|
||||
- `"Radio station not found"`: Station ID or join code not found
|
||||
- `"Only the station owner can..."`: Action requires owner privileges
|
||||
- `"Failed to connect to radio station. Invalid join code or station not found."`: Join code is invalid
|
||||
- `"Client not found"`: Client ID not found
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a Station and Adding Songs
|
||||
|
||||
1. **Create a station:**
|
||||
### Creating a Station
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/radio-stations \
|
||||
|
@ -540,18 +192,7 @@ curl -X POST http://localhost:8080/api/radio-stations \
|
|||
-d '{"name": "My Station", "description": "Great music"}'
|
||||
```
|
||||
|
||||
2. **Use the returned owner token for authenticated requests:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/radio-stations/{stationId}/songs \
|
||||
-H "Authorization: Bearer {ownerToken}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "Song Title", "artist": "Artist", "duration": 180, "url": "http://example.com/song.mp3"}'
|
||||
```
|
||||
|
||||
### Joining a Station and Voting
|
||||
|
||||
1. **Join a station:**
|
||||
### Joining a Station
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/clients/connect \
|
||||
|
@ -559,20 +200,16 @@ curl -X POST http://localhost:8080/api/clients/connect \
|
|||
-d '{"username": "john", "joinCode": "ABC123"}'
|
||||
```
|
||||
|
||||
2. **Vote on a song:**
|
||||
### Getting All Stations (with authentication)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/radio-stations/{stationId}/songs/{songId}/vote \
|
||||
-H "Authorization: Bearer {clientToken}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"voteType": "UPVOTE"}'
|
||||
curl -X GET "http://localhost:8080/api/radio-stations" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
## Notes
|
||||
### Deleting a Station (owner only)
|
||||
|
||||
- Join codes are 6-character alphanumeric strings automatically generated for each station
|
||||
- Songs in the queue are sorted by score (upvotes - downvotes) in descending order
|
||||
- Users can change their vote on a song, but only have one vote per song
|
||||
- Only station owners can control playback (play next song)
|
||||
- Tokens expire after 7 days
|
||||
- The system uses in-memory storage, so data is lost when the server restarts
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8080/api/radio-stations/{stationId} \
|
||||
-H "Authorization: Bearer {ownerToken}"
|
||||
```
|
||||
|
|
|
@ -55,16 +55,8 @@ public class RadioStationController {
|
|||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<RadioStation>>> getAllRadioStations(
|
||||
@RequestParam(defaultValue = "false") boolean activeOnly) {
|
||||
String currentUserId = AuthUtil.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(ApiResponse.error("User not authenticated"));
|
||||
}
|
||||
|
||||
List<RadioStation> stations = activeOnly ? radioStationService.getActiveRadioStations()
|
||||
: radioStationService.getAllRadioStations();
|
||||
public ResponseEntity<ApiResponse<List<RadioStation>>> getAllRadioStations() {
|
||||
List<RadioStation> stations = radioStationService.getAllRadioStations();
|
||||
return ResponseEntity.ok(ApiResponse.success(stations));
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
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.service.RadioStationService;
|
||||
import com.serena.backend.service.JwtService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/songs/")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class SongController {
|
||||
|
||||
@Autowired
|
||||
private RadioStationService radioStationService;
|
||||
|
||||
@Autowired
|
||||
private JwtService jwtService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<Void>> addSong(@RequestBody AddSongRequest request) {
|
||||
if (request.getSong() == null || request.getRadioStationId() == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ApiResponse<>(false, "Song data and radio station ID are required", null));
|
||||
}
|
||||
|
||||
boolean success = radioStationService.addSongToQueue(request.getRadioStationId(), request.getSong());
|
||||
|
||||
if (success) {
|
||||
return ResponseEntity.ok(new ApiResponse<>(true, "Song added to queue successfully", null));
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(new ApiResponse<>(false, "Radio station not found or inactive", null));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.serena.backend.dto;
|
||||
|
||||
import com.serena.backend.model.Song;
|
||||
|
||||
public class AddSongRequest {
|
||||
private Song song;
|
||||
private String radioStationId;
|
||||
|
||||
public AddSongRequest() {}
|
||||
|
||||
public AddSongRequest(Song song, String radioStationId) {
|
||||
this.song = song;
|
||||
this.radioStationId = radioStationId;
|
||||
}
|
||||
|
||||
public Song getSong() {
|
||||
return song;
|
||||
}
|
||||
|
||||
public void setSong(Song song) {
|
||||
this.song = song;
|
||||
}
|
||||
|
||||
public String getRadioStationId() {
|
||||
return radioStationId;
|
||||
}
|
||||
|
||||
public void setRadioStationId(String radioStationId) {
|
||||
this.radioStationId = radioStationId;
|
||||
}
|
||||
}
|
|
@ -8,12 +8,10 @@ public class Client {
|
|||
private String username;
|
||||
private String radioStationId;
|
||||
private LocalDateTime connectedAt;
|
||||
private boolean isActive;
|
||||
|
||||
public Client() {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.connectedAt = LocalDateTime.now();
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
public Client(String username, String radioStationId) {
|
||||
|
@ -55,11 +53,4 @@ public class Client {
|
|||
this.connectedAt = connectedAt;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
isActive = active;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
public class RadioStation {
|
||||
private String id;
|
||||
|
@ -13,15 +15,15 @@ public class RadioStation {
|
|||
private String ownerId;
|
||||
private String joinCode;
|
||||
private LocalDateTime createdAt;
|
||||
private boolean isActive;
|
||||
private List<String> connectedClients;
|
||||
private Queue<Song> songQueue;
|
||||
|
||||
public RadioStation() {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.joinCode = generateJoinCode();
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.isActive = true;
|
||||
this.connectedClients = new ArrayList<>();
|
||||
this.songQueue = new LinkedList<>();
|
||||
}
|
||||
|
||||
public RadioStation(String name, String description, String ownerId) {
|
||||
|
@ -91,14 +93,6 @@ public class RadioStation {
|
|||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
isActive = active;
|
||||
}
|
||||
|
||||
public List<String> getConnectedClients() {
|
||||
return connectedClients;
|
||||
}
|
||||
|
@ -107,4 +101,20 @@ public class RadioStation {
|
|||
this.connectedClients = connectedClients;
|
||||
}
|
||||
|
||||
public Queue<Song> getSongQueue() {
|
||||
return songQueue;
|
||||
}
|
||||
|
||||
public void setSongQueue(Queue<Song> songQueue) {
|
||||
this.songQueue = songQueue;
|
||||
}
|
||||
|
||||
public void addSongToQueue(Song song) {
|
||||
this.songQueue.offer(song);
|
||||
}
|
||||
|
||||
public Song getNextSong() {
|
||||
return this.songQueue.poll();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
60
backend/src/main/java/com/serena/backend/model/Song.java
Normal file
60
backend/src/main/java/com/serena/backend/model/Song.java
Normal file
|
@ -0,0 +1,60 @@
|
|||
package com.serena.backend.model;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class Song {
|
||||
private String id;
|
||||
private int popularity;
|
||||
private double tempo;
|
||||
private Map<String, Double> audioFeatures;
|
||||
|
||||
public Song() {
|
||||
this.audioFeatures = new HashMap<>();
|
||||
}
|
||||
|
||||
public Song(String id, int popularity) {
|
||||
this.id = id;
|
||||
this.popularity = popularity;
|
||||
this.audioFeatures = new HashMap<>();
|
||||
}
|
||||
|
||||
public Song(String id, int popularity, double tempo, Map<String, Double> audioFeatures) {
|
||||
this.id = id;
|
||||
this.popularity = popularity;
|
||||
this.tempo = tempo;
|
||||
this.audioFeatures = audioFeatures != null ? audioFeatures : new HashMap<>();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getPopularity() {
|
||||
return popularity;
|
||||
}
|
||||
|
||||
public void setPopularity(int popularity) {
|
||||
this.popularity = popularity;
|
||||
}
|
||||
|
||||
public double getTempo() {
|
||||
return tempo;
|
||||
}
|
||||
|
||||
public void setTempo(double tempo) {
|
||||
this.tempo = tempo;
|
||||
}
|
||||
|
||||
public Map<String, Double> getAudioFeatures() {
|
||||
return audioFeatures;
|
||||
}
|
||||
|
||||
public void setAudioFeatures(Map<String, Double> audioFeatures) {
|
||||
this.audioFeatures = audioFeatures;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.serena.backend.service;
|
|||
|
||||
import com.serena.backend.model.RadioStation;
|
||||
import com.serena.backend.model.Client;
|
||||
import com.serena.backend.model.Song;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -29,7 +30,7 @@ public class RadioStationService {
|
|||
|
||||
public Optional<RadioStation> getRadioStationByJoinCode(String joinCode) {
|
||||
return radioStations.values().stream()
|
||||
.filter(station -> station.getJoinCode().equals(joinCode) && station.isActive())
|
||||
.filter(station -> station.getJoinCode().equals(joinCode))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
|
@ -37,12 +38,6 @@ public class RadioStationService {
|
|||
return new ArrayList<>(radioStations.values());
|
||||
}
|
||||
|
||||
public List<RadioStation> getActiveRadioStations() {
|
||||
return radioStations.values().stream()
|
||||
.filter(RadioStation::isActive)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public Optional<RadioStation> updateRadioStation(String stationId, String name, String description) {
|
||||
RadioStation station = radioStations.get(stationId);
|
||||
if (station != null) {
|
||||
|
@ -58,7 +53,6 @@ public class RadioStationService {
|
|||
public boolean deleteRadioStation(String stationId) {
|
||||
RadioStation station = radioStations.get(stationId);
|
||||
if (station != null) {
|
||||
station.setActive(false);
|
||||
// Disconnect all clients
|
||||
station.getConnectedClients().clear();
|
||||
// Remove from clients map
|
||||
|
@ -72,7 +66,7 @@ public class RadioStationService {
|
|||
// Client Management
|
||||
public Optional<Client> connectClient(String username, String radioStationId, String joinCode) {
|
||||
RadioStation station = radioStations.get(radioStationId);
|
||||
if (station != null && station.isActive() && station.getJoinCode().equals(joinCode)) {
|
||||
if (station != null && station.getJoinCode().equals(joinCode)) {
|
||||
Client client = new Client(username, radioStationId);
|
||||
clients.put(client.getId(), client);
|
||||
station.getConnectedClients().add(client.getId());
|
||||
|
@ -121,4 +115,21 @@ public class RadioStationService {
|
|||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
public boolean addSongToQueue(String radioStationId, Song song) {
|
||||
RadioStation station = radioStations.get(radioStationId);
|
||||
if (station != null) {
|
||||
station.addSongToQueue(song);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Optional<Song> getNextSong(String radioStationId) {
|
||||
RadioStation station = radioStations.get(radioStationId);
|
||||
if (station != null) {
|
||||
return Optional.ofNullable(station.getNextSong());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,154 +0,0 @@
|
|||
package com.serena.backend.service;
|
||||
|
||||
import com.serena.backend.model.RadioStation;
|
||||
import com.serena.backend.model.Client;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RadioStationServiceTest {
|
||||
|
||||
private RadioStationService radioStationService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
radioStationService = new RadioStationService();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create radio station with join code")
|
||||
void shouldCreateRadioStationWithJoinCode() {
|
||||
// Given
|
||||
String name = "Test Station";
|
||||
String description = "Test Description";
|
||||
String ownerId = "owner123";
|
||||
|
||||
// When
|
||||
RadioStation station = radioStationService.createRadioStation(name, description, ownerId);
|
||||
|
||||
// Then
|
||||
assertNotNull(station);
|
||||
assertNotNull(station.getId());
|
||||
assertNotNull(station.getJoinCode());
|
||||
assertEquals(6, station.getJoinCode().length());
|
||||
assertEquals(name, station.getName());
|
||||
assertEquals(description, station.getDescription());
|
||||
assertEquals(ownerId, station.getOwnerId());
|
||||
assertTrue(station.isActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find radio station by join code")
|
||||
void shouldFindRadioStationByJoinCode() {
|
||||
// Given
|
||||
RadioStation station = radioStationService.createRadioStation("Test Station", "Description", "owner123");
|
||||
String joinCode = station.getJoinCode();
|
||||
|
||||
// When
|
||||
Optional<RadioStation> foundStation = radioStationService.getRadioStationByJoinCode(joinCode);
|
||||
|
||||
// Then
|
||||
assertTrue(foundStation.isPresent());
|
||||
assertEquals(station.getId(), foundStation.get().getId());
|
||||
assertEquals(joinCode, foundStation.get().getJoinCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should not find radio station with invalid join code")
|
||||
void shouldNotFindRadioStationWithInvalidJoinCode() {
|
||||
// Given
|
||||
radioStationService.createRadioStation("Test Station", "Description", "owner123");
|
||||
String invalidJoinCode = "INVALID";
|
||||
|
||||
// When
|
||||
Optional<RadioStation> foundStation = radioStationService.getRadioStationByJoinCode(invalidJoinCode);
|
||||
|
||||
// Then
|
||||
assertFalse(foundStation.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should connect client with valid join code")
|
||||
void shouldConnectClientWithValidJoinCode() {
|
||||
// Given
|
||||
RadioStation station = radioStationService.createRadioStation("Test Station", "Description", "owner123");
|
||||
String username = "testuser";
|
||||
String joinCode = station.getJoinCode();
|
||||
|
||||
// When
|
||||
Optional<Client> client = radioStationService.connectClient(username, station.getId(), joinCode);
|
||||
|
||||
// Then
|
||||
assertTrue(client.isPresent());
|
||||
assertEquals(username, client.get().getUsername());
|
||||
assertEquals(station.getId(), client.get().getRadioStationId());
|
||||
assertTrue(station.getConnectedClients().contains(client.get().getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should not connect client with invalid join code")
|
||||
void shouldNotConnectClientWithInvalidJoinCode() {
|
||||
// Given
|
||||
RadioStation station = radioStationService.createRadioStation("Test Station", "Description", "owner123");
|
||||
String username = "testuser";
|
||||
String invalidJoinCode = "WRONG1";
|
||||
|
||||
// When
|
||||
Optional<Client> client = radioStationService.connectClient(username, station.getId(), invalidJoinCode);
|
||||
|
||||
// Then
|
||||
assertFalse(client.isPresent());
|
||||
assertTrue(station.getConnectedClients().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should connect client using join code only")
|
||||
void shouldConnectClientUsingJoinCodeOnly() {
|
||||
// Given
|
||||
RadioStation station = radioStationService.createRadioStation("Test Station", "Description", "owner123");
|
||||
String username = "testuser";
|
||||
String joinCode = station.getJoinCode();
|
||||
|
||||
// When
|
||||
Optional<Client> client = radioStationService.connectClientByJoinCode(username, joinCode);
|
||||
|
||||
// Then
|
||||
assertTrue(client.isPresent());
|
||||
assertEquals(username, client.get().getUsername());
|
||||
assertEquals(station.getId(), client.get().getRadioStationId());
|
||||
assertTrue(station.getConnectedClients().contains(client.get().getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should not connect client to inactive radio station")
|
||||
void shouldNotConnectClientToInactiveRadioStation() {
|
||||
// Given
|
||||
RadioStation station = radioStationService.createRadioStation("Test Station", "Description", "owner123");
|
||||
station.setActive(false);
|
||||
String username = "testuser";
|
||||
String joinCode = station.getJoinCode();
|
||||
|
||||
// When
|
||||
Optional<Client> client = radioStationService.connectClient(username, station.getId(), joinCode);
|
||||
|
||||
// Then
|
||||
assertFalse(client.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate unique join codes for different stations")
|
||||
void shouldGenerateUniqueJoinCodesForDifferentStations() {
|
||||
// Given & When
|
||||
RadioStation station1 = radioStationService.createRadioStation("Station 1", "Desc 1", "owner1");
|
||||
RadioStation station2 = radioStationService.createRadioStation("Station 2", "Desc 2", "owner2");
|
||||
|
||||
// Then
|
||||
assertNotEquals(station1.getJoinCode(), station2.getJoinCode());
|
||||
assertEquals(6, station1.getJoinCode().length());
|
||||
assertEquals(6, station2.getJoinCode().length());
|
||||
}
|
||||
}
|
4
frontend/src/constants/ApiConstants.js
Normal file
4
frontend/src/constants/ApiConstants.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const RADIOSTATION_URL = "http://localhost:8080/api";
|
||||
|
||||
export const CREATE_RADIOSTATION_ENDPOINT = "/radio-stations";
|
||||
export const LIST_RADIOSTATIONS_ENDPOINT = "/radio-stations";
|
|
@ -1,11 +1,14 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState } from "react";
|
||||
import { createStation } from "../utils/StationsCreate";
|
||||
|
||||
// I UNDERSTAND THIS!! --Noah
|
||||
|
||||
function CreateStation() {
|
||||
const [joinMethod, setJoinMethod] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
const handleCreateStation = () => {
|
||||
console.log('Creating station with password:', password);
|
||||
createStation(name, description);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -35,52 +38,18 @@ function CreateStation() {
|
|||
<h1>Create a Station on Serena</h1>
|
||||
</header>
|
||||
|
||||
<main className="create-station-form">
|
||||
<div className="join-method-section">
|
||||
<h2>How should people be able to join your station?</h2>
|
||||
|
||||
<div className="radio-option">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="joinMethod"
|
||||
value="password"
|
||||
checked={joinMethod === 'password'}
|
||||
onChange={(e) => setJoinMethod(e.target.value)}
|
||||
/>
|
||||
<span className="radio-custom"></span>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<main className="create-station-content">
|
||||
<textarea onChange={(e) => setName(e.target.value)} />
|
||||
<textarea onChange={(e) => setDescription(e.target.value)} />
|
||||
|
||||
{joinMethod === 'password' && (
|
||||
<div className="password-input-section">
|
||||
<label htmlFor="station-password">Station Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="station-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter station password"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="create-station-final-btn"
|
||||
onClick={handleCreateStation}
|
||||
disabled={joinMethod !== 'password' || !password.trim()}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
Create Radio Station
|
||||
</button>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="create-station-final-btn"
|
||||
onClick={handleCreateStation}
|
||||
disabled={!name || !description}
|
||||
>
|
||||
Create Radio Station
|
||||
</button>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { getStations } from "../utils/GetStations";
|
||||
|
||||
function JoinStation() {
|
||||
const [verifyMethod, setVerifyMethod] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const [stations, setStations] = useState([]);
|
||||
|
||||
const handleJoinStation = () => {
|
||||
console.log('Joining station with password:', password);
|
||||
// Redirect to station page after joining
|
||||
navigate('/station');
|
||||
const handleJoinStation = (stationID) => {
|
||||
console.log("Joining station with ID:" + stationID);
|
||||
navigate("/station");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Test");
|
||||
|
||||
getStations(getStations());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="join-station-container">
|
||||
<div className="join-station-main">
|
||||
|
@ -39,52 +44,15 @@ function JoinStation() {
|
|||
<h1>Join a Station on Serena</h1>
|
||||
</header>
|
||||
|
||||
<main className="join-station-form">
|
||||
<div className="verify-method-section">
|
||||
<h2>How would you like to verify access?</h2>
|
||||
|
||||
<div className="radio-option">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="verifyMethod"
|
||||
value="password"
|
||||
checked={verifyMethod === 'password'}
|
||||
onChange={(e) => setVerifyMethod(e.target.value)}
|
||||
/>
|
||||
<span className="radio-custom"></span>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{verifyMethod === 'password' && (
|
||||
<div className="password-input-section">
|
||||
<label htmlFor="station-password">Station Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="station-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter station password"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="join-station-final-btn"
|
||||
onClick={handleJoinStation}
|
||||
disabled={verifyMethod !== 'password' || !password.trim()}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||
</svg>
|
||||
Join Radio Station
|
||||
</button>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main className="join-station-content">
|
||||
{stations.map((station, index) => {
|
||||
return (
|
||||
<div className="verify-method-section">
|
||||
<text>station</text>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
20
frontend/src/utils/GetStations.js
Normal file
20
frontend/src/utils/GetStations.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {
|
||||
LIST_RADIOSTATIONS_ENDPOINT,
|
||||
RADIOSTATION_URL,
|
||||
} from "../constants/ApiConstants";
|
||||
|
||||
export async function getStations() {
|
||||
fetch(RADIOSTATION_URL + LIST_RADIOSTATIONS_ENDPOINT, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("Station:", data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error creating station:", error);
|
||||
});
|
||||
}
|
28
frontend/src/utils/StationsCreate.js
Normal file
28
frontend/src/utils/StationsCreate.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import {
|
||||
CREATE_RADIOSTATION_ENDPOINT,
|
||||
RADIOSTATION_URL,
|
||||
} from "../constants/ApiConstants";
|
||||
|
||||
// I UNDERSTAND THIS :D --Noah
|
||||
|
||||
export async function createStation(name, description) {
|
||||
const body = {
|
||||
name: "My Awesome Station",
|
||||
description: "The best music station ever",
|
||||
};
|
||||
|
||||
fetch(RADIOSTATION_URL + CREATE_RADIOSTATION_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("Station created:", data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error creating station:", error);
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue