Skip to content

Commit

Permalink
SpotifyPlayList-dev-019-recommend-with-playlist (#182)
Browse files Browse the repository at this point in the history
* BE service add getSongFeatureByPlayList, add recommend with playList

* controller add getSongFeatureWithPlayList endpoint, service fix res init

* implement getRecommendationWithPlayList, move nb, update readme

* update

* update attr in dto, refactor RecommendationsService, add getRecommendWithPlaylist endpoint

* FE add GetRecommendationWithPlaylist view, update router, app.vue

* update FE view, add logging at FE, BE

* FE fix view naming, placeholder name

* FE fix v-model, naming

* BE fix spotify client init, fix recommend reqeust set seed features

* FE fix playlist hardcode, remove no need code

* recommend get seed song from random song, PlayListService clean code

* BE remove no need log, commented code
  • Loading branch information
yennanliu authored Oct 27, 2024
1 parent de1c21f commit 131cda7
Show file tree
Hide file tree
Showing 11 changed files with 7,017 additions and 584 deletions.
2 changes: 2 additions & 0 deletions springSpotifyPlayList/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ docker logs <container_id>

- Java client
- https://github.com/spotify-web-api-java/spotify-web-api-java
- Python client
- https://github.com/spotipy-dev/spotipy
- Doc
- https://spotify-web-api-java.github.io/spotify-web-api-java/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import se.michaelthelin.spotify.model_objects.special.SnapshotResult;
import se.michaelthelin.spotify.model_objects.specification.AudioFeatures;
import se.michaelthelin.spotify.model_objects.specification.Playlist;

import java.util.List;

@Slf4j
@RestController
@RequestMapping("/playlist")
Expand All @@ -35,6 +38,18 @@ public ResponseEntity getPlayListWithId(@PathVariable("playListId") String playL
}
}

@GetMapping("/songFeature/{playListId}")
public ResponseEntity getSongFeatureWithPlayListWithId(@PathVariable("playListId") String playListId) {

try {
List<AudioFeatures> audioFeaturesList = playListService.getSongFeatureByPlayList(playListId);
return ResponseEntity.status(HttpStatus.OK).body(audioFeaturesList);
} catch (Exception e) {
log.error("getSongFeatureByPlayList error : " + e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}

@PostMapping("/create")
public ResponseEntity CreatePlayList(@RequestBody CreatePlayListDto createPlayListDto) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import se.michaelthelin.spotify.model_objects.specification.Recommendations;

@Slf4j
Expand All @@ -22,7 +19,6 @@ public class RecommendationsController {

@PostMapping("/")
public ResponseEntity getRecommendation(@RequestBody GetRecommendationsDto getRecommendationsDto) {

try {
log.info("(getRecommendation) getRecommendationsDto = " + getRecommendationsDto.toString());
Recommendations recommendations = recommendationsService.getRecommendation(getRecommendationsDto);
Expand All @@ -33,4 +29,16 @@ public ResponseEntity getRecommendation(@RequestBody GetRecommendationsDto getRe
}
}

@GetMapping("/playlist/{playListId}")
public ResponseEntity getRecommendationWithPlayList(@PathVariable("playListId") String playListId) {
try {
log.info("(getRecommendationWithPlayList) playListId = " + playListId);
Recommendations recommendations = recommendationsService.getRecommendationWithPlayList(playListId);
return ResponseEntity.status(HttpStatus.OK).body(recommendations);
} catch (Exception e) {
log.error("getRecommendationWithPlayList error : " + e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.yen.SpotifyPlayList.model.dto;

import com.neovisionaries.i18n.CountryCode;
import lombok.Data;
import lombok.ToString;

/**
*
* {
* acousticness: 0.359,
* analysisUrl: "https://api.spotify.com/v1/audio-analysis/7FJC2pF6zMliU7Lvk0GBDV",
* danceability: 0.337,
* durationMs: 358587,
* energy: 0.532,
* id: "7FJC2pF6zMliU7Lvk0GBDV",
* instrumentalness: 0,
* key: 6,
* liveness: 0.0827,
* loudness: -6.296,
* mode: "MAJOR",
* speechiness: 0.0345,
* tempo: 140.257,
* timeSignature: 4,
* trackHref: "https://api.spotify.com/v1/tracks/7FJC2pF6zMliU7Lvk0GBDV",
* type: "AUDIO_FEATURES",
* uri: "spotify:track:7FJC2pF6zMliU7Lvk0GBDV",
* valence: 0.336
* },
*
*/

@ToString
@Data
public class GetRecommendationsWithFeatureDto {

private int amount = 10;
private CountryCode market = CountryCode.JP;
private int maxPopularity = 100;
private int minPopularity = 0;
private String seedArtistId; // e.g. : 0LcJLqbBmaGUft1e9Mm8HV
private String seedGenres;
private String seedTrack; // e.g. 01iyCAUm8EvOFqVWYJ3dVX
private int targetPopularity = 50;
private double danceability = 0;
private double energy = 0;
private double instrumentalness = 0;
private double liveness = 0;
private double loudness = 0;
private double speechiness = 0;
private double tempo = 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,20 @@
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.special.SnapshotResult;
import se.michaelthelin.spotify.model_objects.specification.AudioFeatures;
import se.michaelthelin.spotify.model_objects.specification.Paging;
import se.michaelthelin.spotify.model_objects.specification.Playlist;
import se.michaelthelin.spotify.model_objects.specification.PlaylistTrack;
import se.michaelthelin.spotify.requests.data.playlists.AddItemsToPlaylistRequest;
import se.michaelthelin.spotify.requests.data.playlists.CreatePlaylistRequest;
import se.michaelthelin.spotify.requests.data.playlists.GetPlaylistRequest;
import se.michaelthelin.spotify.requests.data.playlists.GetPlaylistsItemsRequest;
import se.michaelthelin.spotify.requests.data.tracks.GetAudioFeaturesForTrackRequest;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

@Service
@Slf4j
Expand Down Expand Up @@ -107,6 +115,29 @@ public Playlist createPlayList(CreatePlayListDto createPlayListDto) throws Spoti
return playlist;
}

public List<AudioFeatures> getSongFeatureByPlayList(String playListId) {

log.debug("getSongFeatureByPlayList start, playListId = " + playListId);
List<AudioFeatures> audioFeaturesList = new ArrayList<>();
try {
SpotifyApi spotifyApi = authService.initializeSpotifyApi();
final GetPlaylistsItemsRequest getPlaylistsItemsRequest = spotifyApi
.getPlaylistsItems(playListId)
.build();
final CompletableFuture<Paging<PlaylistTrack>> pagingFuture = getPlaylistsItemsRequest.executeAsync();
final Paging<PlaylistTrack> playlistTrackPaging = pagingFuture.join();
for (PlaylistTrack track : playlistTrackPaging.getItems()) {
final GetAudioFeaturesForTrackRequest getAudioFeaturesForTrackRequest = spotifyApi
.getAudioFeaturesForTrack(track.getTrack().getId())
.build();
audioFeaturesList.add(getAudioFeaturesForTrackRequest.execute());
}
} catch (Exception e) {
throw new RuntimeException("getSongFeatureByPlayList error: " + e);
}
return audioFeaturesList;
}

/**
* Adds a song to a playlist.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,151 @@
package com.yen.SpotifyPlayList.service;

import com.yen.SpotifyPlayList.model.dto.GetRecommendationsDto;
import com.yen.SpotifyPlayList.model.dto.GetRecommendationsWithFeatureDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.core5.http.ParseException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.specification.AudioFeatures;
import se.michaelthelin.spotify.model_objects.specification.Recommendations;
import se.michaelthelin.spotify.requests.data.browse.GetRecommendationsRequest;

import java.io.IOException;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;

@Service
@Slf4j
public class RecommendationsService {

@Autowired
private AuthService authService;
@Autowired private AuthService authService;

private SpotifyApi spotifyApi;
@Autowired private PlayListService playListService;

public RecommendationsService() {
private SpotifyApi spotifyApi;

public RecommendationsService() {}

public Recommendations getRecommendation(GetRecommendationsDto getRecommendationsDto)
throws SpotifyWebApiException {
try {
this.spotifyApi = authService.initializeSpotifyApi();
GetRecommendationsRequest getRecommendationsRequest =
prepareRecommendationsRequest(getRecommendationsDto);
Recommendations recommendations = getRecommendationsRequest.execute();
log.info("Fetched recommendations: {}", recommendations);
return recommendations;
} catch (IOException | SpotifyWebApiException | ParseException e) {
log.error("Error fetching recommendations: {}", e.getMessage());
throw new SpotifyWebApiException("getRecommendation error: " + e.getMessage());
}
}

public Recommendations getRecommendationWithPlayList(String playListId)
throws SpotifyWebApiException {
try {
this.spotifyApi = authService.initializeSpotifyApi();
List<AudioFeatures> audioFeaturesList = playListService.getSongFeatureByPlayList(playListId);
log.debug(">>> audioFeaturesList = " + audioFeaturesList);

// Use functional programming to calculate the averages and cast Double to float
// TODO : modify GetRecommendationsWithFeatureDto with attr as "float" type, and modify below
// code (e.g. : averagingDouble)
double energy =
audioFeaturesList.stream().collect(Collectors.averagingDouble(AudioFeatures::getEnergy));
double acousticness =
audioFeaturesList.stream()
.collect(Collectors.averagingDouble(AudioFeatures::getAcousticness));
double danceability =
audioFeaturesList.stream()
.collect(Collectors.averagingDouble(AudioFeatures::getDanceability));
double liveness =
audioFeaturesList.stream()
.collect(Collectors.averagingDouble(AudioFeatures::getLiveness));
double loudness =
audioFeaturesList.stream()
.collect(Collectors.averagingDouble(AudioFeatures::getLoudness));
double speechiness =
audioFeaturesList.stream()
.collect(Collectors.averagingDouble(AudioFeatures::getSpeechiness));

GetRecommendationsWithFeatureDto featureDto = new GetRecommendationsWithFeatureDto();
featureDto.setEnergy(energy);
// featureDto.setAcousticness(acousticness);
featureDto.setDanceability(danceability);
featureDto.setLiveness(liveness);
featureDto.setLoudness(loudness);
featureDto.setSpeechiness(speechiness);

// TODO : get seed features from playList
featureDto.setSeedArtistId("4sJCsXNYmUMeumUKVz4Abm");
featureDto.setSeedTrack(getRandomSeedTrackId(audioFeaturesList));

GetRecommendationsRequest getRecommendationsRequest =
prepareRecommendationsRequestWithPlayList(featureDto);
Recommendations recommendations = getRecommendationsRequest.execute();

return recommendations;
} catch (Exception e) {
log.error("Error fetching recommendations with playlist features: {}", e.getMessage());
throw new SpotifyWebApiException("getRecommendationWithPlayList error: " + e.getMessage());
}
}

private GetRecommendationsRequest prepareRecommendationsRequestWithPlayList(
GetRecommendationsWithFeatureDto featureDto)
throws IOException, SpotifyWebApiException, ParseException {
return spotifyApi
.getRecommendations()
.limit(featureDto.getAmount())
.market(featureDto.getMarket())
.max_popularity(featureDto.getMaxPopularity())
.min_popularity(featureDto.getMinPopularity())
.seed_artists(featureDto.getSeedArtistId())
.seed_genres(featureDto.getSeedGenres())
.seed_tracks(featureDto.getSeedTrack())
// TODO : undo float cast once modify GetRecommendationsWithFeatureDto with attr as "float" type
.target_danceability((float) featureDto.getDanceability())
.target_energy((float) featureDto.getEnergy())
.target_instrumentalness((float) featureDto.getInstrumentalness())
.target_liveness((float) featureDto.getLiveness())
.target_loudness((float) featureDto.getLoudness())
.target_speechiness((float) featureDto.getSpeechiness())
.build();
}

private GetRecommendationsRequest prepareRecommendationsRequest(GetRecommendationsDto dto) {
return spotifyApi
.getRecommendations()
.limit(dto.getAmount())
.market(dto.getMarket())
.max_popularity(dto.getMaxPopularity())
.min_popularity(dto.getMinPopularity())
.seed_artists(dto.getSeedArtistId())
.seed_genres(dto.getSeedGenres())
.seed_tracks(dto.getSeedTrack())
.target_popularity(dto.getTargetPopularity())
.build();
}

public Recommendations getRecommendation(GetRecommendationsDto getRecommendationsDto) throws SpotifyWebApiException {

Recommendations recommendations = null;
try {
this.spotifyApi = authService.initializeSpotifyApi();
GetRecommendationsRequest getRecommendationsRequest = prepareRecommendationsRequest(getRecommendationsDto);
recommendations = getRecommendationsRequest.execute();
log.info("recommendations = " + recommendations);
} catch (IOException | SpotifyWebApiException | ParseException e) {
throw new SpotifyWebApiException("getRecommendation error: " + e.getMessage());
}
return recommendations;
private String getRandomSeedTrackId(List<AudioFeatures> audioFeaturesList) {
if (audioFeaturesList == null || audioFeaturesList.size() == 0) {
throw new RuntimeException("audioFeaturesList can not be null");
}
Random random = new Random();
int randomInt = random.nextInt(audioFeaturesList.size());
String trackHref = audioFeaturesList.get(randomInt).getTrackHref();
return getTrackIdFromTrackUrl(trackHref);
}

private GetRecommendationsRequest prepareRecommendationsRequest(GetRecommendationsDto getRecommendationsDto){
return spotifyApi.getRecommendations()
.limit(getRecommendationsDto.getAmount())
.market(getRecommendationsDto.getMarket())
.max_popularity(getRecommendationsDto.getMaxPopularity())
.min_popularity(getRecommendationsDto.getMinPopularity())
.seed_artists(getRecommendationsDto.getSeedArtistId())
.seed_genres(getRecommendationsDto.getSeedGenres())
.seed_tracks(getRecommendationsDto.getSeedTrack())
.target_popularity(getRecommendationsDto.getTargetPopularity())
.build();
private String getTrackIdFromTrackUrl(String trackUrl) {
if (trackUrl == null) {
throw new RuntimeException("trackUrl can not be null");
}
return trackUrl.split("tracks")[1].replace("/", "");
}

}
Loading

0 comments on commit 131cda7

Please sign in to comment.