Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions springSpotifyPlayList/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a full-stack Spotify playlist application with ML-based song recommendations. The application integrates with the Spotify Web API to provide features like song search, playlist creation, and intelligent music recommendations.

**Architecture:**
- Backend: Spring Boot (Java 8) with Maven
- Frontend: Vue.js 2 with Vue CLI
- Deployment: Docker Compose
- Integration: Spotify Web API via spotify-web-api-java library

## Development Commands

### Backend (Spring Boot)
```bash
# Navigate to backend directory
cd backend/SpotifyPlayList

# Build the application
mvn clean package

# Run tests
mvn test

# Run the application locally
mvn spring-boot:run

# Skip tests during build
mvn clean package -DskipTests

# Run specific test class
mvn test -Dtest=AlbumControllerTest
```

### Frontend (Vue.js)
```bash
# Navigate to frontend directory
cd frontend/spotify-playlist-ui

# Install dependencies
npm install

# Run development server
npm run serve

# Build for production
npm run build

# Run linter
npm run lint
```

### Docker Development
```bash
# Set required environment variables
export SPOTIFY_CLIENT_SECRET=<your_spotify_client_secret>
export SPOTIFY_REDIRECT_URL=http://<server_ip>:8080/playlist

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The placeholder <server_ip> in the SPOTIFY_REDIRECT_URL might be confusing for local development setup. It would be clearer to specify localhost for local environments and add a note that this needs to be replaced with the actual server IP for deployment. For example: export SPOTIFY_REDIRECT_URL=http://localhost:8080/playlist # Use localhost for local dev, replace with server IP for deployment.

export VUE_APP_BASE_URL=http://localhost:8888/

# Run full stack application
docker-compose up

# For EC2 deployment with sudo
sudo -E docker-compose up

# Clean up Docker resources
docker rm -f $(docker ps -aq)
docker rmi -f $(docker images -q)
docker system prune
```

## Application Configuration

### Required Spotify API Setup
1. Register at [Spotify Developer Platform](https://developer.spotify.com/documentation/web-api)
2. Update `spotify.client.secret` and `spotify.client.id` in `backend/SpotifyPlayList/src/main/resources/application.properties`
3. Configure redirect URL in Spotify app settings to match `spotify.redirect.url`
4. Update `baseURL` in `frontend/spotify-playlist-ui/src/App.vue` for frontend API calls

### Key Configuration Files
- **Backend Config:** `backend/SpotifyPlayList/src/main/resources/application.properties`
- **Frontend Config:** `frontend/spotify-playlist-ui/src/App.vue` (baseURL configuration)
Comment on lines +81 to +85

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hardcoding the baseURL in App.vue (as mentioned on lines 81 and 85) is not a recommended practice for maintainability. It makes configuration for different environments (development, staging, production) cumbersome and error-prone. A better approach would be to use environment variables. Vue CLI supports .env files out of the box. You could have a .env.development with VUE_APP_BASE_URL=http://localhost:8888/ and a .env.production with the production API URL. The code can then access it via process.env.VUE_APP_BASE_URL. This would eliminate the need for manual code changes when deploying.

- **Docker Config:** `docker-compose.yml` with environment variable mappings

## Project Structure

### Backend Architecture
- **Controllers:** Handle HTTP requests and OAuth flow (`controller/` package)
- **Services:** Business logic for Spotify API integration (`service/` package)
- **Models/DTOs:** Data transfer objects and response models (`model/` package)
- **Config:** Web configuration and CORS setup (`config/` package)

Key service classes:
- `AuthService`: Spotify OAuth authentication and token management
- `RecommendationsService`: ML-based song recommendation logic
- `PlayListService`: Playlist creation and management
- `SearchService`: Artist, album, and track search functionality

### Frontend Architecture
- **Views:** Main application pages (`views/` directory)
- **Components:** Reusable UI components (`components/` directory)
- **Router:** Vue Router configuration (`router/index.js`)

Key views:
- `GetRecommendation.vue`: ML recommendation interface
- `CreatePlayList.vue`: Playlist creation functionality
- Search views for albums, artists, and tracks

## API Endpoints

- **Backend API:** http://localhost:8888/swagger-ui.html
- **Frontend UI:** http://localhost:8080
- **Production ports:** Backend on 8888, Frontend on 8080

## Testing

The project includes comprehensive test coverage:
- **Unit Tests:** Service layer tests with Mockito
- **Integration Tests:** Controller tests for API endpoints
- **Development Tests:** Manual API testing classes in `test/java/.../dev/`

Run backend tests with `mvn test` or specific test classes with `mvn test -Dtest=<TestClassName>`.

## Dependencies

### Key Backend Dependencies
- Spring Boot 2.4.5 (Web, Test, RestTemplate)
- spotify-web-api-java 8.3.6 (Legacy - used for auth/playlist/search only)
- **Direct HTTP Integration**: Recommendations API uses RestTemplate for direct Spotify API calls
- Swagger 2.7.0 (API documentation)
- Mockito 5.2.0 (Testing)
- Lombok (Code generation)

## Important Architecture Notes

### Recommendation API Implementation
The recommendation functionality (`RecommendationsService`) has been **modernized to use direct HTTP calls** instead of the spotify-web-api-java library due to compatibility issues:

- **New Classes:**
- `SpotifyHttpClient` - HTTP request builder and client utilities
- `SpotifyRecommendationsResponse` - Complete DTOs for Spotify API responses with all fields
- `LegacyRecommendationsResponse` - Frontend-compatible response format
- `RecommendationsResponseMapper` - Comprehensive mapping between formats
- `RecommendationsValidator` - Response validation and data integrity checks
- `SpotifyErrorHandler` - Enhanced error handling with detailed error messages
- `SpotifyApiException` - Custom exception with status codes and context

- **Modified Services:**
- `RecommendationsService` - Now uses RestTemplate + response mapper
- `RecommendationsController` - Updated to use legacy response format
- `WebConfig` - Added RestTemplate bean with error handler

- **Endpoints Affected:** `/recommend/` and `/recommend/playlist/{id}`
- **Frontend Compatibility:** ✅ Complete - maintains exact response structure
- Preserves `tracks.tracks[]` array structure
- Maintains `externalUrls.externalUrls.spotify` nested format
- Converts `preview_url` to `previewUrl` camelCase
- All existing frontend code works without changes

### Enhanced Response Mapping (Phase 4)
**Complete Field Coverage:**
- ✅ All Spotify API fields mapped: `restrictions`, `linked_from`, `available_markets`
- ✅ Enhanced error responses with helpful context and user-friendly messages
- ✅ Comprehensive validation for data integrity and frontend compatibility
- ✅ Edge case handling: null fields, empty responses, malformed data
- ✅ Robust error categorization: auth errors, rate limits, bad requests
- ✅ 17 comprehensive tests covering all scenarios and error conditions

### Key Frontend Dependencies
- Vue.js 2.6.14

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Vue.js 2 has reached its End of Life (EOL) on December 31, 2023. Continuing to use an EOL version is a security risk as it will no longer receive security updates or bug fixes. It is strongly recommended to prioritize migrating the frontend application to Vue 3 to ensure the project's security and long-term maintainability.

- Vue Router 3.5.1
- Axios 1.6.8 (HTTP client)
- SweetAlert 2.1.2 (UI notifications)

## Quick Update Shortcuts

### Adding Information to CLAUDE.md
Use this shortcut pattern to quickly add important information:

```bash
# Quick CLAUDE.md update pattern:
# 1. Identify the section (Architecture, Dependencies, Commands, etc.)
# 2. Use Edit tool with specific section markers
# 3. Always maintain existing structure

# Example: Adding new architecture notes
Edit CLAUDE.md -> Find "## Important Architecture Notes" -> Add after existing content

# Example: Adding new dependencies
Edit CLAUDE.md -> Find "### Key Backend Dependencies" -> Add to list

# Example: Adding new commands
Edit CLAUDE.md -> Find "### Backend (Spring Boot)" -> Add new command with description
```

### Common Update Patterns:
1. **New Service/Class**: Add to "Important Architecture Notes" section
2. **New Command**: Add to appropriate "Development Commands" section
3. **Configuration Change**: Update "Application Configuration" section
4. **Dependency Change**: Update "Dependencies" section
5. **Endpoint Change**: Update "API Endpoints" section

### Template for Architecture Updates:
```markdown
- **New Feature/Service**: Brief description
- `ClassName` - What it does and why it's important
- Key methods or functionality
- Integration points with existing services
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

Problematic Areas:
- RecommendationsService:42 - Uses spotify-web-api-java library's
GetRecommendationsRequest.execute()
- RecommendationsService:94 - Same issue for playlist-based recommendations
- Both methods rely on se.michaelthelin.spotify classes that are failing

Current Flow:
1. Frontend sends POST to /recommend/ or GET to /recommend/playlist/{id}
2. RecommendationsController calls RecommendationsService
3. Service uses Java library to build and execute Spotify API requests
4. Returns se.michaelthelin.spotify.model_objects.specification.Recommendations object

Implementation Plan

Phase 1: Create Direct HTTP Client Infrastructure
1. Add HTTP Dependencies: Include RestTemplate or WebClient configuration in Spring
Boot
2. Create Response DTOs: Build custom response classes to replace Recommendations
object
3. Create Request Builder: Utility class to construct proper Spotify API URLs and
headers

Phase 2: Replace Service Methods
1. Replace getRecommendation():
- Build HTTP GET request to https://api.spotify.com/v1/recommendations
- Map GetRecommendationsDto parameters to query string
- Handle OAuth token from AuthService
2. Replace getRecommendationWithPlayList():
- Keep existing playlist analysis logic (calculating averages)
- Replace only the final Spotify API call with direct HTTP

Phase 3: Maintain Existing Interfaces
1. Controller Compatibility: Keep same endpoints and request/response structure
2. Frontend Compatibility: No changes needed to Vue.js components
3. DTO Compatibility: Reuse existing GetRecommendationsDto and
GetRecommendationsWithFeatureDto

Phase 4: Response Mapping
1. Custom Response Objects: Create POJOs matching Spotify API JSON response
2. Backward Compatibility: Ensure response structure matches what frontend expects
3. Error Handling: Map HTTP errors to existing exception handling

Key Technical Details

Spotify API Endpoint:
- URL: GET https://api.spotify.com/v1/recommendations
- Auth: Bearer token from existing AuthService
- Query Parameters: Map from existing DTOs

Required Changes:
- RecommendationsService.java: Replace library calls with HTTP clients
- Add new response DTOs under model/dto/Response/
- Keep all existing controller endpoints unchanged
- Maintain integration with AuthService for token management

Benefits:
- Direct control over API calls
- No dependency on potentially broken Java library
- Easier debugging and customization
- Better error handling and logging

This approach isolates the fix to recommendation functionality only, preserving all
other Spotify integrations and maintaining full backward compatibility with the
frontend.
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package com.yen.SpotifyPlayList.config;

import com.yen.SpotifyPlayList.service.SpotifyErrorHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

// TODO : check if can merge below
// 1) enable CORS 2) show swagger 2.x UI properly
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private SpotifyErrorHandler spotifyErrorHandler;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
Expand All @@ -31,4 +37,11 @@ public void addCorsMappings(CorsRegistry registry) {
}
};
}

@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(spotifyErrorHandler);
return restTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.yen.SpotifyPlayList.controller;

import com.yen.SpotifyPlayList.exception.SpotifyApiException;
import com.yen.SpotifyPlayList.model.dto.GetRecommendationsDto;
import com.yen.SpotifyPlayList.model.dto.Response.LegacyRecommendationsResponse;
import com.yen.SpotifyPlayList.service.RecommendationsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import se.michaelthelin.spotify.model_objects.specification.Recommendations;

@Slf4j
@RestController
Expand All @@ -21,23 +22,31 @@ public class RecommendationsController {
public ResponseEntity getRecommendation(@RequestBody GetRecommendationsDto getRecommendationsDto) {
try {
log.info("(getRecommendation) getRecommendationsDto = " + getRecommendationsDto.toString());
Recommendations recommendations = recommendationsService.getRecommendation(getRecommendationsDto);
LegacyRecommendationsResponse recommendations = recommendationsService.getRecommendation(getRecommendationsDto);
return ResponseEntity.status(HttpStatus.OK).body(recommendations);
} catch (SpotifyApiException e) {
log.error("getRecommendation Spotify API error: {}", e.getMessage());
HttpStatus status = HttpStatus.valueOf(e.getStatusCode());
return ResponseEntity.status(status).body(e.getMessage());
} catch (Exception e) {
log.error("getRecommendation error : " + e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
log.error("getRecommendation unexpected error: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
Comment on lines 31 to +33

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Exposing raw exception messages to the client via e.getMessage() can be a security risk, as it might reveal internal application details. It's better to log the full exception for debugging and return a generic error message to the user.

Suggested change
} catch (Exception e) {
log.error("getRecommendation error : " + e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
log.error("getRecommendation unexpected error: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
} catch (Exception e) {
log.error("getRecommendation unexpected error: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred while fetching recommendations.");

}
}

@GetMapping("/playlist/{playListId}")
public ResponseEntity getRecommendationWithPlayList(@PathVariable("playListId") String playListId) {
try {
log.info("(getRecommendationWithPlayList) playListId = " + playListId);
Recommendations recommendations = recommendationsService.getRecommendationWithPlayList(playListId);
LegacyRecommendationsResponse recommendations = recommendationsService.getRecommendationWithPlayList(playListId);
return ResponseEntity.status(HttpStatus.OK).body(recommendations);
} catch (SpotifyApiException e) {
log.error("getRecommendationWithPlayList Spotify API error: {}", e.getMessage());
HttpStatus status = HttpStatus.valueOf(e.getStatusCode());
return ResponseEntity.status(status).body(e.getMessage());
} catch (Exception e) {
log.error("getRecommendationWithPlayList error : " + e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
log.error("getRecommendationWithPlayList unexpected error: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
Comment on lines 47 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the other endpoint, exposing raw exception messages via e.getMessage() can be a security risk. A generic error message should be returned to the client, while the detailed error is logged internally.

Suggested change
} catch (Exception e) {
log.error("getRecommendationWithPlayList error : " + e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
log.error("getRecommendationWithPlayList unexpected error: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
} catch (Exception e) {
log.error("getRecommendationWithPlayList unexpected error: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred while fetching recommendations.");

}
}

Expand Down
Loading