Merge branch 'main' into noah-dev
This commit is contained in:
commit
4ea9fea362
14 changed files with 223 additions and 572 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,41 @@
|
|||
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.AddSongToStationRequest;
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
30
backend/src/main/java/com/serena/backend/model/Song.java
Normal file
30
backend/src/main/java/com/serena/backend/model/Song.java
Normal file
|
@ -0,0 +1,30 @@
|
|||
package com.serena.backend.model;
|
||||
|
||||
public class Song {
|
||||
private String id;
|
||||
private int popularity;
|
||||
|
||||
public Song() {}
|
||||
|
||||
public Song(String id, int popularity) {
|
||||
this.id = id;
|
||||
this.popularity = popularity;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -39,7 +40,6 @@ public class RadioStationService {
|
|||
|
||||
public List<RadioStation> getActiveRadioStations() {
|
||||
return radioStations.values().stream()
|
||||
.filter(RadioStation::isActive)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,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 +71,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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
# How the Spotify API registration works
|
||||
|
||||
[Spotify Docs](https://developer.spotify.com/documentation/web-api/tutorials/getting-started#create-an-app)
|
||||
[Spotify Docs](https://developer.spotify.com/documentation/web-api/tutorials/getting-started#create-app)
|
||||
|
||||
## Creating an App with their developer console
|
||||
|
||||
|
@ -9,7 +8,11 @@ https://developer.spotify.com/dashboard
|
|||
|
||||
*App* name: Serena
|
||||
*App description*: open source radio station emulator to get the vibe right
|
||||
*redirect URI*: https://localhost:3000
|
||||
*redirect URI*:
|
||||
- Development: http://127.0.0.1:3000/callback
|
||||
- Production: https://your-domain.com/callback
|
||||
|
||||
**Important**: Spotify doesn't accept `localhost` URLs. Use `127.0.0.1` for local development.
|
||||
|
||||
## Requesting an Access Token
|
||||
|
||||
|
|
30
frontend/src/components/LoginButton.jsx
Normal file
30
frontend/src/components/LoginButton.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
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 (
|
||||
<div className="login-status">
|
||||
<span>✓ Connected to Spotify</span>
|
||||
<button onClick={handleLogout} className="logout-btn">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={getSpotifyLoginUrl()}>
|
||||
<button className="spotify-login-btn">Login with Spotify</button>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginButton;
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { createStation } from '../utils/StationsCreate';
|
||||
import React, { useState } from "react";
|
||||
import { createStation } from "../utils/StationsCreate";
|
||||
|
||||
// I UNDERSTAND THIS!! --Noah
|
||||
|
||||
|
@ -21,7 +21,7 @@ function CreateStation() {
|
|||
<textarea onChange={(e) => setName(e.target.value)} />
|
||||
<textarea onChange={(e) => setDescription(e.target.value)} />
|
||||
|
||||
<button
|
||||
<button
|
||||
className="create-station-final-btn"
|
||||
onClick={handleCreateStation}
|
||||
disabled={!name || !description}
|
||||
|
@ -33,5 +33,4 @@ function CreateStation() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
export default CreateStation;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import AddSongModal from './AddSongModal';
|
||||
import LoginButton from '../components/LoginButton';
|
||||
|
||||
function StationPage() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
@ -44,6 +45,7 @@ function StationPage() {
|
|||
<header className="station-header">
|
||||
<h1>Serena Station</h1>
|
||||
<p className="station-subtitle">Collaborative Music Experience</p>
|
||||
<LoginButton />
|
||||
</header>
|
||||
|
||||
<div className="media-controls-section">
|
||||
|
|
40
frontend/src/utils/spotifyAuth.js
Normal file
40
frontend/src/utils/spotifyAuth.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
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();
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue